'use strict';

// ----------------- Constants -----------------------------
const FOXYPROXY_BASIC = false;

// Bit-wise flags so we can add/remove these independently. We may add more later so PROTOCOL_ALL is future-proof.
const PROTOCOL_ALL = 1; // in case other protocols besides http and https are supported later
const PROTOCOL_HTTP = 2;
const PROTOCOL_HTTPS = 4;


// import | pac
const PROXY_TYPE_HTTP = 1;
const PROXY_TYPE_HTTPS = 2;
const PROXY_TYPE_SOCKS5 = 3;
const PROXY_TYPE_SOCKS4 = 4;
const PROXY_TYPE_NONE = 5; // DIRECT
const PROXY_TYPE_PAC = 6;
const PROXY_TYPE_WPAD = 7;
const PROXY_TYPE_SYSTEM = 8;
const PROXY_TYPE_PASS = 9;


const PATTERN_TYPE_WILDCARD = 1;
const PATTERN_TYPE_REGEXP = 2;

// Storage keys that are not proxy settings
const NON_PROXY_KEYS = ['mode', 'logging', 'sync', 'browserVersion', 'foxyProxyVersion', 'foxyProxyEdition', 'nextIndex'];

// bg | import | proxy | utils
const PATTERN_ALL_WHITE = {
  title: 'all URLs',
  active: true,
  pattern: '*',
  type: 1,                    // PATTERN_TYPE_WILDCARD,
  protocols: 1                // PROTOCOL_ALL
};

const DEFAULT_COLOR = '#66cc66'; // default proxy color

// patterns | proxy
// the local-internal blacklist, always used as a set
const blacklistSet = [
  {
    title: "local hostnames (usually no dots in the name). Pattern exists because 'Do not use this proxy for localhost and intranet/private IP addresses' is checked.",
    pattern: "^(?:[^:@/]+(?::[^@/]+)?@)?(?:localhost|127\\.\\d+\\.\\d+\\.\\d+)(?::\\d+)?(?:/.*)?$",
  },
  {
    title: "local subnets (IANA reserved address space). Pattern exists because 'Do not use this proxy for localhost and intranet/private IP addresses' is checked.",
    pattern: "^(?:[^:@/]+(?::[^@/]+)?@)?(?:192\\.168\\.\\d+\\.\\d+|10\\.\\d+\\.\\d+\\.\\d+|172\\.(?:1[6789]|2[0-9]|3[01])\\.\\d+\\.\\d+)(?::\\d+)?(?:/.*)?$",
  },
  {
    title: "localhost - matches the local host optionally prefixed by a user:password authentication string and optionally suffixed by a port number. The entire local subnet (127.0.0.0/8) matches. Pattern exists because 'Do not use this proxy for localhost and intranet/private IP addresses' is checked.",
    pattern: "^(?:[^:@/]+(?::[^@/]+)?@)?[\\w-]+(?::\\d+)?(?:/.*)?$"
  }
].map (item => {
  item.active = true;
  item.type = 2;              // PATTERN_TYPE_REGEXP,
  item.protocols = 1;         // PROTOCOL_ALL
  return item;
});

// ----------------- Utils ---------------------------------
class Utils {

  static notify(message, title = 'FoxyProxy') {
    // the id is not used anywhere and can be omitted, it is only useful if you want to manually close the notification early
    chrome.notifications.create('foxyproxy', {
      type: 'basic',
      iconUrl: '/images/icon.svg',
      title,
      message
    });
  }

  // options | popup
  static isUnsupportedType(type) {
    //return type === PROXY_TYPE_PAC || type === PROXY_TYPE_WPAD || type === PROXY_TYPE_SYSTEM || type === PROXY_TYPE_PASS;
    return [PROXY_TYPE_PAC, PROXY_TYPE_WPAD, PROXY_TYPE_SYSTEM, PROXY_TYPE_PASS].includes(type);
  }

  // bg | pattern-tester | validate-pattern
  static wildcardToRegExp(pat) {

    let start = 0, end = pat.length, matchOptionalSubdomains = false;

    if (pat[0] === '.') { pat = '*' + pat; }

    if (pat.startsWith('**')) {
      // Strip asterisks from front and back
      while (pat[start] === '*' && start < end) start++;
      while (pat[end - 1] === '*' && start < end) end--;
      // If there's only an asterisk left, match everything
      if (end - start == 1 && pat[start] == '*') return '';
    }
    else if (pat.startsWith('*.')) { matchOptionalSubdomains = true; }

    let regExpStr = pat.substring(start, end+1)
      // $& replaces with the string found, but with that string escaped
      .replace(/[$.+()^{}\]\[|]/g, '\\$&')
      .replace(/\*/g, '.*')
      .replace(/\?/g, '.');

    if (matchOptionalSubdomains) {
        // Non-capturing group that matches:
        // any group of non-whitespace characters following by an optional . repeated zero or more times
        regExpStr = '(?:\\S+\\.)*' + regExpStr.substring(4);
    }

    // Leading or ending double-asterisks mean exact starting and ending positions
    if (start === 0) { regExpStr = '^' + regExpStr; }
    if (end === pat.length) { regExpStr += '$'; }
    return regExpStr;
  }

  // Prep the patternObject for matching: convert wildcards to regexp,
  // store the originalPattern which the user entered so we can display if needed, etc.
  // Return null if patternObject is inactive or there is an error.
	static processPatternObject(patternObject) {
    if (patternObject.active) {
      // Store the original pattern so if this pattern matches something,
      // we can display whatever the user entered ("original") in the log.
      patternObject.originalPattern = patternObject.pattern;
      if (patternObject.type === PATTERN_TYPE_WILDCARD) {
        patternObject.pattern = Utils.wildcardToRegExp(patternObject.pattern);
      }
      try {
        // Convert to real RegExp, not just a string. Validate. If invalid, notify user.
        patternObject.pattern = new RegExp(patternObject.pattern, 'i');
        return patternObject;
      }
      catch(e) {
  			console.error(`Error creating regexp for pattern: ${patternObject.pattern}`, e);
  			Utils.notify(`Error creating regular expression for pattern ${regExpStr}`);
  		}
    }
    return null;
	}

  // import | pattern
  static importFile(file, mimeTypeArr, maxSizeBytes, jsonOrXml, callback) {

    if (!file) {
      alert('There was an error');
      return;
    }

    // Check MIME type // Ch65 no filetype for JSON
    if (!mimeTypeArr.includes(file.type)) {
      alert('Unsupported file format');
      return;
    }

    if (file.size > maxSizeBytes) {
      alert('Filesize is too large');
      return;
    }

    const reader  = new FileReader();
    reader.onloadend = () => {
      if (reader.error) {
        alert('Error reading file.');
        return;
      }

      let settings;
      try {
        if (jsonOrXml === 'json') { settings = JSON.parse(reader.result); }
        else if (jsonOrXml === 'xml') {
          settings = new DOMParser().parseFromString(reader.result, 'text/xml');
          if (settings.documentElement.nodeName === 'parsererror') { throw new Error(); }
        }
      }
      catch(e) {
        console.log(e);
        alert("Error parsing file. Please remove sensitive data from the file, and then email it to support@getfoxyproxy.org so we can fix bugs in our parser.");
        return;
      }
      if (settings && confirm('This will overwite existing proxy settings. Are you sure?')) { callback(settings); }
      else { callback(); }

    };
    reader.onerror = () => { alert('Error reading file'); };
    reader.readAsText(file);
  }

  // import | options
  static exportFile() {

    chrome.storage.local.get(null, result => {
      browser.runtime.getBrowserInfo().then((bi) => {
        !result.sync ? Utils.saveAs(result, bi.version) : chrome.storage.sync.get(null, result => {
          Utils.saveAs(result, bi.version, true);
        });
      });
    });
  }
  // exportFile helper
  static saveAs(data, browserVersion, sync) {

    const settings = data; //Utils.prepareForSettings(data);
    // Browser version and extension version. These are used for debugging.
    settings.browserVersion = browserVersion;
    settings.foxyProxyVersion = chrome.runtime.getManifest().version;
    settings.foxyProxyEdition = FOXYPROXY_BASIC ? 'basic' : 'standard';
    settings.sync = sync;
    const blob = new Blob([JSON.stringify(settings, null, 2)], {type : 'text/plain;charset=utf-8'});
    const filename = chrome.i18n.getMessage('extensionName') + '_' + new Date().toISOString().substring(0, 10) + '.json';
    chrome.downloads.download({
      url: URL.createObjectURL(blob),
      filename,
      saveAs: true,
      conflictAction: 'uniquify'
    });
  }

  static updateIcon(iconPath, color, title, titleIsKey, badgeText, badgeTextIsKey) {
    chrome.browserAction.setIcon({path: iconPath});
    if (color) {
      chrome.browserAction.setBadgeBackgroundColor({color: color});
    }
    else {
      // TODO: confirm this is OK to do
      chrome.browserAction.setBadgeBackgroundColor({color: null});
    }
    if (title) {
      chrome.browserAction.setTitle({title: 'FoxyProxy: ' + (titleIsKey ? chrome.i18n.getMessage(title) : title)});
    }
    else {
      chrome.browserAction.setTitle({title: ''});
    }
    if (badgeText) {
      chrome.browserAction.setBadgeText({text: badgeTextIsKey ? chrome.i18n.getMessage(badgeText) : badgeText});
    }
    else {
      chrome.browserAction.setBadgeText({text: ''});
    }
  }

  static getProxyTitle(proxySetting) {
    if (proxySetting.title) {
      return proxySetting.title;
    }
    else if (proxySetting.type === PROXY_TYPE_NONE) {
      return 'Direct (no proxy)';
    }
    else {
      return `${proxySetting.address}:${proxySetting.port}`;
    }
  }

/*
  // utils only used for export, will be removed as DB format export is adapted
  static prepareForSettings(settings = {}) {

    //if (settings && !settings.mode) { }// 5.0 settings

    let lastResortFound = false;
    const prefKeys = Object.keys(settings);

    const def = {
      id: LASTRESORT,
      active: true,
      title: 'Default',
      notes: 'These are the settings that are used when no patterns match a URL.',
      color: '#0055E5',
      type: PROXY_TYPE_NONE,
      whitePatterns: [PATTERN_ALL_WHITE],
      blackPatterns: []
    };

    // base format
    const ret = {
      mode: 'disabled',
      proxySettings: [],
      logging: {
        size: 500,
        active: true
      }
    };

    if (!prefKeys.length) {                                     // settings is {}
      ret.proxySettings = [def];
      return ret;
    }

    prefKeys.forEach(key => {

      switch (key) {

        case 'mode':
        case 'logging':
          ret[key] = settings[key];
          break;

        case 'sync': break;                                 // do nothing

        default:
          const temp = settings[key];
          temp.id = key;                                    // Copy the id into the object
          temp.id === LASTRESORT && (lastResortFound = true);
          ret.proxySettings.push(temp);
      }
    });

    ret.proxySettings.sort((a, b) => a.index - b.index);
    ret.proxySettings.forEach(item => delete item.index);   // Re-calculated when/if this object is written to disk again (user may move proxySetting up/down)

    !lastResortFound && ret.proxySettings.push(def);        // add default lastresort

    return ret;
  }
*/

  static getUniqueId() {
    // We don't need cryptographically secure UUIDs, just something unique
    return Math.random().toString(36).substring(7) + new Date().getTime();
  }

  static stripBadChars(str) {
    return str ? str.replace(/[&<>"']+/g, '') : null;
  }

}
