package org.cwepg.hr;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.nio.channels.Channels;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.TreeSet;
import org.cwepg.reg.FusionRegistryEntry;
import org.cwepg.reg.Registry;
import org.cwepg.reg.RegistryHelperFusion;
import org.cwepg.svc.HdhrCommandLine;
import org.cwepg.svc.HtmlVcrDoc;
public class TunerManager {
	
	Map<String, Tuner> tuners = new TreeMap<String, Tuner>();
	static TunerManager tunerManager;
    private String lastReason = "";
	Set<String> capvSet = new TreeSet<String>();
    Properties externalProps;
    private ArrayList<Tuner> nonResponsiveTuners = new ArrayList<Tuner>();
    public static String fusionInstalledLocation;
    private static boolean mCountingTuners = false;
    public static final int VIRTUAL_MATCHING = 0;
    public static final int NAME_MATCHING = 1;
    public static boolean skipFusionInit = false;
    public static boolean skipRegistryForTesting = false;
    
    
	private TunerManager(){
	    if (!skipFusionInit) {
            fusionInstalledLocation = RegistryHelperFusion.getInstalledLocation();
            if  (fusionInstalledLocation == null) fusionInstalledLocation = CaptureManager.cwepgPath;
            if ("".equals(fusionInstalledLocation)) fusionInstalledLocation = new File("test").getAbsoluteFile().getParentFile().getAbsolutePath();
	    }
	}
	
	public static TunerManager getInstance(){
		if (tunerManager == null){
			tunerManager = new TunerManager();
		}
		return tunerManager;
	}
    
	public int countTuners(){
	    mCountingTuners = true;
        // First, read tuners from machine (no alteration of our current list of tuners)
        ArrayList<Tuner> refreshedTuners = new ArrayList<Tuner>();
        boolean addDevice = false;
        List<Tuner> hdhrTunerList = countTunersHdhr(addDevice);
        for (Iterator<Tuner> iter = hdhrTunerList.iterator(); iter.hasNext();) {refreshedTuners.add(iter.next());}
        List<Tuner> myhdTunerList = countTunersMyhd(addDevice);
        for (Iterator<Tuner> iter = myhdTunerList.iterator(); iter.hasNext();) {refreshedTuners.add(iter.next());}
        List<?> fusionTunerList = countTunersFusion(addDevice, false);
        for (Iterator<?> iter = fusionTunerList.iterator(); iter.hasNext();) {refreshedTuners.add((Tuner)iter.next());}
        // DRS 20110619 - Added 2 - externalTuner
        List<Tuner> externalTunerList = countTunersExternal(addDevice);
        for (Iterator<Tuner> iter = externalTunerList.iterator(); iter.hasNext();) {refreshedTuners.add(iter.next());}
        
        // Next, loop through existing tuners looking for changed/deleted tuners
        ArrayList<Tuner> deletedTuners = new ArrayList<Tuner>();
        for (Iterator<String> iter = tuners.keySet().iterator(); iter.hasNext();) {
            Tuner existingTuner = tuners.get(iter.next());
            if (refreshedTuners.contains(existingTuner)){
                // The two lists contain the same item.
                // This means we don't need to do anything.
                // We just remove the tuner from the refreshedTuners list
                // (since anything left in the list, we will be adding later).
                refreshedTuners.remove(existingTuner);
                // but the old (existing tuner) might have different lineup, so refresh that
                refreshLineup(existingTuner); // THIS CLEARS ANY EXISTING CHANNELS
            } else {
                // this existing tuner is changed or deleted
                // see if we can find it by name
                boolean found = false;
                for (Tuner tuner : refreshedTuners) {
                    if (tuner.getFullName().equals(existingTuner.getFullName())){
                        // take the attributes off of the tuner we just created
                        // and update the existing tuner.
                        found = true;
                        existingTuner.setAnalogFileExtension(tuner.getAnalogFileExtension());
                        existingTuner.setLiveDevice(tuner.getLiveDevice());
                        existingTuner.setRecordPath(tuner.getRecordPath());
                        // We just remove the tuner from the refreshedTuners list
                        // (since anything left in the list, we will be adding later
                        // and we don't want to add this because the existing tuner
                        // just got updated with what we needed).
                        refreshedTuners.remove(existingTuner);
                        // but the old (existing tuner) might have different lineup, so refresh that
                        refreshLineup(existingTuner);
                    }
                    if (found) break;
                }
                if (!found){
                    // The latest and best list from our recent refresh did not
                    // include the tuner, we must conclude it's gone now, so 
                    // we will delete it shortly.
                    deletedTuners.add(existingTuner);
                }
            }
        }
        
        //remove any deleted tuners
        for (Tuner tuner : deletedTuners) {
            System.out.println(new Date() + " Removing deleted tuner: " + tuner.getFullName());
            this.tuners.remove(tuner.getFullName());
            tuner.removeAllCaptures(true); //before deleting a tuner, delete it's captures
        }
        
        
        // DRS 20210415 - Added 'for' loop + 1 - Concurrent Modification Exception
        for (Tuner tuner : nonResponsiveTuners) {
            System.out.println(new Date() + " Removing non-responsive tuner: " + tuner.getFullName());
            this.tuners.remove(tuner.getFullName());
            tuner.removeAllCaptures(true); //before deleting a tuner, delete it's captures
        }
        nonResponsiveTuners.clear();
        
        // any tuners left in the refreshed list need to be added to the tuner manager
        for (Tuner tuner : refreshedTuners) {
            System.out.println(new Date() + " Adding new or changed: " + tuner.getFullName());
            // refreshed tuners are not added to the tuner manager and did not pick-up captures from file, so do both
            this.tuners.put(tuner.getFullName(), tuner);
            tuner.addCapturesFromStore();
            try {refreshLineup(tuner);} catch (Throwable t) {System.out.println(new Date() + " Problem refreshing lineup on new or changed tuner " + t.getMessage());};
        }
        mCountingTuners = false;
        System.out.println(new Date() + " TunerManager.countTuners returned " + this.tuners.size() + " tuners.");
        return this.tuners.size();
	}
	
	// DRS 20190422 - Added method
	// DRS 20210415 - Changed non-responsive tuners to instance variable (delete later) - Concurrent Modification Exception
    public void removeHdhrByUrl(String url) {
        nonResponsiveTuners = new ArrayList<Tuner>();
        for (Entry<String, Tuner> entry : this.tuners.entrySet()) {
            Tuner aTuner = entry.getValue();
            if (aTuner instanceof TunerHdhr) {
                TunerHdhr hdhrTuner = (TunerHdhr)aTuner;
                if(url.contains(hdhrTuner.ipAddressTuner)) {
                    nonResponsiveTuners.add(hdhrTuner);
                }
            }
        }
        // DRS 20210415 - Commented 'for' loop - Concurrent Modification Exception
        //for (Tuner tuner : deletedTuners) {
        //    System.out.println(new Date() + " Removing non-responsive tuner: " + tuner.getFullName());
        //    this.tuners.remove(tuner.getFullName());
        //    tuner.removeAllCaptures(true); //before deleting a tuner, delete it's captures
        //}
        
    }
    
    private void refreshLineup(Tuner existingTuner) {
        try {
            existingTuner.scanRefreshLineUp(true, existingTuner.lineUp.signalType, 10000);
        } catch (Throwable e){
            String msg = new Date() + " ERROR: Could not refresh lineup for an existing tuner " + existingTuner;
            System.out.println(msg);
            System.err.println(msg);
            e.printStackTrace();
        }
    }
    /* This Method Only Used in Testing */
    public void countTuner(int tunerType, boolean addDevice){
        removeAllTuners();
        switch (tunerType) {
        case Tuner.FUSION_TYPE:
            countTunersFusion(addDevice, false);
            break;
        case Tuner.MYHD_TYPE:
            countTunersMyhd(addDevice);
            break;
        case Tuner.HDHR_TYPE:
            countTunersHdhr(addDevice);
            break;
        case Tuner.EXTERNAL_TYPE:
            countTunersExternal(addDevice);
            break;
        }
        return;
    }
    
    public List<Tuner> countTunersFusion(boolean addDevice, boolean test){
        ArrayList<Tuner> tunerList = new ArrayList<Tuner>();
        if (TunerManager.skipFusionInit) return tunerList;
        String controlSetName = "CurrentControlSet";
        if (test) controlSetName = "ControlSet0002";
        Map<String, FusionRegistryEntry> entries = RegistryHelperFusion.getFusionRegistryEntries(controlSetName);
        try {
            int analogFileExtensionNumber = Registry.getIntValue("HKEY_CURRENT_USER", "Software\\Dvico\\ZuluHDTV\\Data", "AnalogRecProfile");
            
            Map<String, Map> lookupTables = TunerManager.getLookupTables();
            if (lookupTables == null) return tunerList;  // DRS 20210114 - Added 1 - If we get an null here, return empty list and stop any more attempts...all hope is lost.
            Map<String, String> recordPathsByNumber = lookupTables.get("recordPathsByNumber");
            Map<String, String> recordPathsByName = lookupTables.get("recordPathsByName");
            Map<String, String> names = lookupTables.get("names");
            // if the names array has a matching uinumber, then we apply the name, else we keep default
            for (Iterator<String> iter = entries.keySet().iterator(); iter.hasNext();) {
                FusionRegistryEntry entry = entries.get(iter.next());
                entry.setNameUsingKey(names);
                entry.setRecordPathUsingKey(recordPathsByNumber, recordPathsByName);
                entry.setAnalogFileExtensionNumber(analogFileExtensionNumber);
                System.out.println(entry);
                tunerList.add(new TunerFusion(entry, false));
            }
        } catch (Exception e) {
            System.out.println(new Date() + " ERROR: Problem with countTunersFusion: " + e.getMessage());
            System.err.println(new Date() + " ERROR: Problem with countTunersFusion: " + e.getMessage());
            e.printStackTrace();
        }
        return tunerList;
    }
    
    public static Map<String, Map> getLookupTables() {
        HashMap<String, String> recordPathsByNumber = new HashMap<String, String>();
        HashMap<String, String> recordPathsByName = new HashMap<String, String>();
        HashMap<String, String> names = new HashMap<String, String>();
        
        /**** Get Data from the Data or DeviceN branch(es) if they exist ****/
        try {
            // record path for single device registry
            String[] registryBranchSingle = {"HKEY_CURRENT_USER", "Software\\Dvico\\ZuluHDTV\\Data", ""};
            if (Registry.valueExists(registryBranchSingle[0],registryBranchSingle[1],"DeviceMainUID")){
                String recordPathEntry = Registry.getStringValue(registryBranchSingle[0], registryBranchSingle[1], "RecordPath");
                String deviceMainUidEntry = "" + Registry.getIntValue(registryBranchSingle[0], registryBranchSingle[1], "DeviceMainUID");
                String modelNameEntry = Registry.getStringValue(registryBranchSingle[0], registryBranchSingle[1], "ModelName");
                System.out.println(registryBranchSingle[1] +  "\\RecordPath=" + recordPathEntry);
                System.out.println(registryBranchSingle[1] +  "\\DeviceMainUID=" + deviceMainUidEntry);
                System.out.println(registryBranchSingle[1] +  "\\ModelName=" + modelNameEntry);
                recordPathsByNumber.put (deviceMainUidEntry , recordPathEntry);
                recordPathsByName.put (modelNameEntry, recordPathEntry);
            }
            
            // record paths and names for multiple device registry
            for (int i = 1; i < 5; i++){
                String[] registryBranch = {"HKEY_CURRENT_USER", "Software\\Dvico\\ZuluHDTV\\Data\\Device" + i,""};
                if (Registry.valueExists(registryBranch[0],registryBranch[1],"UINumber")){
                    String recordPathEntry = Registry.getStringValue(registryBranch[0], registryBranch[1], "RecordPath");
                    String modelNameEntry = Registry.getStringValue(registryBranch[0], registryBranch[1], "ModelName");
                    String uiNumberEntry = "" + Registry.getIntValue(registryBranch[0],registryBranch[1],"UINumber");
                    System.out.println(registryBranch[1] +  "\\RecordPath=" + recordPathEntry);
                    System.out.println(registryBranch[1] +  "\\UINumber=" + uiNumberEntry);
                    System.out.println(registryBranch[1] +  "\\ModelName=" + modelNameEntry);
                    names.put (uiNumberEntry, modelNameEntry);
                    recordPathsByNumber.put (uiNumberEntry, recordPathEntry);
                    recordPathsByName.put (modelNameEntry, recordPathEntry);
                } else {
                    break;
                }
            }
        } catch (UnsupportedEncodingException e1) {
            System.out.println(new Date() + " ERROR: Failed to get data from DeviceN branch:" + e1.getMessage());
            System.err.println(new Date() + " ERROR: Failed to get data from DeviceN branch:" + e1.getMessage());
            e1.printStackTrace();
        }
        // Just for debugging
        for (Iterator<String> iterator = names.keySet().iterator(); iterator.hasNext();) {
            String uinumber = iterator.next();
            String name = names.get(uinumber);
            String recordPath = recordPathsByNumber.get(uinumber); 
            System.out.println(new Date() + " Names after DeviceN Branch(es): " + name + "." + uinumber + "  RecordPath:" + recordPath);
        }
        /**** Get Possible Names from Fusion Table ****/
        Connection connection = null;
        Statement statement = null;
        ResultSet rs = null;
        String mdbFileName = "Epg2List.Mdb";
        String localPathFile = fusionInstalledLocation + "\\" + mdbFileName;
        String tableName = "DeviceList";
        if (!(new File(localPathFile).exists()))System.out.println(new Date() + " WARNING: Fusion database file " + localPathFile + " does not exist.  Fusion tuner naming might be impared.");
        try {
            //connection = DriverManager.getConnection("jdbc:odbc:Driver={M icroSoft Access Driver (*.mdb)};DBQ=" + localPathFile);
            connection = DriverManager.getConnection("jdbc:ucanaccess://" + localPathFile + ";singleConnection=true");
            statement = connection.createStatement();
            String sql = "select * from " + tableName;
            System.out.println(new Date() + " " + sql);
            rs = statement.executeQuery(sql);
            while (rs.next()){
                int devId = rs.getInt("devId");
                String devNm = rs.getString("devNm");
                int parenLoc = devNm.indexOf(")");
                if (parenLoc > -1 && parenLoc < devNm.length())
                    devNm = devNm.substring(parenLoc + 1);
                names.put("" + devId, devNm);
            }
            statement.close();
            connection.close();
        } catch (SQLException e) {
            System.out.println(new Date() + " ERROR: TunerManager.getLookupTables:" + e.getMessage());
            //System.err.println(new Date() + " ERROR: TunerManager.countTunersFusion:" + e.getMessage());
            //e.printStackTrace();
            return null; // DRS 20210114 - Added 1 - If we get an error here, return null and stop any more attempts...all hope is lost.
        } finally {
            try { if (rs != null) rs.close(); } catch (Throwable t){}; 
            try { if (statement != null) statement.close(); } catch (Throwable t){}; 
            try { if (connection != null) connection.close(); } catch (Throwable t){}; 
        }
        // Just for debugging
        for (Iterator<String> iterator = names.keySet().iterator(); iterator.hasNext();) {
            String uinumber = iterator.next();
            String name = names.get(uinumber);
            System.out.println(new Date() + " Names after Epg2List: " + name + "." + uinumber);
        }
        
        /****** Save the data and return *********/
        HashMap<String, Map> lookupTables = new HashMap<String, Map>();
        lookupTables.put("recordPathsByNumber", recordPathsByNumber);
        lookupTables.put("recordPathsByName", recordPathsByName);
        lookupTables.put("names", names);
        return lookupTables;
    }
    
    private List<Tuner> countTunersMyhd(boolean addDevice){
        ArrayList<Tuner> tunerList = new ArrayList<Tuner>();
        // if registry entry exists, presume the tuner is still there
        String recordPath = null;
        try {
            if (TunerManager.skipRegistryForTesting) return tunerList;
            recordPath = Registry.getStringValue("HKEY_LOCAL_MACHINE", "SOFTWARE\\MyHD", "HD_DIR_NAME_FOR_RESCAP");
            int i = 0;
            while (recordPath == null && i < 12){
                try {Thread.sleep(250);} catch (Exception e){};
                recordPath = Registry.getStringValue("HKEY_LOCAL_MACHINE", "SOFTWARE\\MyHD", "HD_DIR_NAME_FOR_RESCAP");
                i++;
            }
            if (recordPath != null){
                Tuner tuner = new TunerMyhd(recordPath, addDevice); // automatically added to TunerManager list
                tuner.liveDevice = true;
                tunerList.add(tuner);
            } else {
                System.out.println(new Date() + " No MyHD registry data found.");
            }
        } catch (Exception e){
            System.out.println(new Date() + " No MyHD registry data found." + e.getMessage());
        }
        return tunerList;
    }
    
    // DRS 20120315 - Altered method - if there are valid captures, then wait, try again, and set liveDevice.
    public List<Tuner> countTunersHdhr(boolean addDevice){
        ArrayList<Tuner> tunerList = new ArrayList<Tuner>(); // To be returned from this method.  Live (including retry to get live), and not disabled.
        if (!new File(CaptureManager.hdhrPath + File.separator + "hdhomerun_config.exe").exists()){
            System.out.println("Could not find [" + new File(CaptureManager.hdhrPath + File.separator + "hdhomerun_config.exe" + "]"));
            return tunerList;   // if no exe exists, return an empty tuner list without doing any work. 
        }
        
        TreeSet<String> devices = new TreeSet<String>(); // Working list.  May include non-live to start with, added from discover.txt.
        // Get file devices (from last discover.txt file)
        String fileDiscoverText = getFileDiscoverText();
        ArrayList<String> fileDevices = findTunerDevicesFromText(fileDiscoverText, false);
        System.out.println(new Date() + " Got " + fileDevices.size() + " items from discover.txt");
        devices.addAll(fileDevices);
        // Get live devices
        String liveDiscoverText = getLiveDiscoverText(CaptureManager.discoverRetries, CaptureManager.discoverDelay);
        ArrayList<String> liveDevices = findTunerDevicesFromText(liveDiscoverText, true); // devices.txt is always written out here (we read the old one already).
        System.out.println(new Date() + " Got " + liveDevices.size() + " items from active discover command.");
        devices.addAll(liveDevices);
        System.out.println(new Date() + " Total of  " + devices.size() + " items, accounting for duplication.");
        
        // Only if we picked-up a different device from the discover.txt file do we go into this logic that eliminates devices that do not come alive on retry
        if (liveDevices.size() < devices.size() ) {
            devices = eliminateDevicesThatDoNotComeAliveOnRetry(devices, liveDevices, fileDevices);
            // Now "devices" contains all live devices, even ones that had to be retried to get them going.
            // devices.txt is written out with whatever the result was after retrying
        }
        // DRS 20181103 - Adding IP address to HDHR tuners
        HashMap<String, String> ipAddressMap = getIpMap(fileDiscoverText, liveDiscoverText);
        // DRS 20181025 - Adding model to HDHR tuners
        HashMap<String, Integer> liveModelMap = getLiveModelMap(liveDevices, 1, CaptureManager.discoverDelay, ipAddressMap);
        
        // Loop final list of devices


Answer:         for (String device : devices) {