import * as operations from './operations';
import Settings, { DefaultSettingJSONText } from './settings/Settings';
import Keymaps from './settings/Keymaps';
import Search from './settings/Search';
import Properties from './settings/Properties';
import Blacklist from './settings/Blacklist';

export class FormKeymaps {
  private readonly data: {[op: string]: string};

  private constructor(data: {[op: string]: string}) {
    this.data = data;
  }

  toKeymaps(): Keymaps {
    let keymaps: { [key: string]: operations.Operation } = {};
    for (let name of Object.keys(this.data)) {
      let [type, argStr] = name.split('?');
      let args = {};
      if (argStr) {
        args = JSON.parse(argStr);
      }
      let key = this.data[name];
      keymaps[key] = operations.valueOf({ type, ...args });
    }
    return Keymaps.fromJSON(keymaps);
  }

  toJSON(): {[op: string]: string} {
    return this.data;
  }

  buildWithOverride(op: string, keys: string): FormKeymaps {
    let newData = {
      ...this.data,
      [op]: keys,
    };
    return new FormKeymaps(newData);
  }

  static fromJSON(o: ReturnType<FormKeymaps['toJSON']>): FormKeymaps {
    let data: {[op: string]: string} = {};
    for (let op of Object.keys(o)) {
      data[op] = o[op] as string;
    }
    return new FormKeymaps(data);
  }

  static fromKeymaps(keymaps: Keymaps): FormKeymaps {
    let json = keymaps.toJSON();
    let data: {[op: string]: string} = {};
    for (let key of Object.keys(json)) {
      let op = json[key];
      let args = { ...op };
      delete args.type;

      let name = op.type;
      if (Object.keys(args).length > 0) {
        name += '?' + JSON.stringify(args);
      }
      data[name] = key;
    }
    return new FormKeymaps(data);
  }
}

export class FormSearch {
  private readonly default: string;

  private readonly engines: string[][];

  constructor(defaultEngine: string, engines: string[][]) {
    this.default = defaultEngine;
    this.engines = engines;
  }

  toSearchSettings(): Search {
    let engines: { [name: string]: string } = {};
    for (let entry of this.engines) {
      engines[entry[0]] = entry[1];
    }
    return new Search(this.default, engines);
  }

  toJSON(): {
    default: string;
    engines: string[][];
    } {
    return {
      default: this.default,
      engines: this.engines,
    };
  }

  static fromJSON(o: ReturnType<FormSearch['toJSON']>): FormSearch {
    if (!Object.prototype.hasOwnProperty.call(o, 'default')) {
      throw new TypeError(`"default" field not set`);
    }
    if (!Object.prototype.hasOwnProperty.call(o, 'engines')) {
      throw new TypeError(`"engines" field not set`);
    }
    return new FormSearch(o.default, o.engines);
  }

  static fromSearch(search: Search): FormSearch {
    let engines = Object.entries(search.engines).reduce(
      (o: string[][], [name, url]) => {
        return o.concat([[name, url]]);
      }, []);
    return new FormSearch(search.defaultEngine, engines);
  }
}

export class JSONTextSettings {
  constructor(
    private json: string,
  ) {
  }

  toSettings(): Settings {
    return Settings.fromJSON(JSON.parse(this.json));
  }

  toJSONText(): string {
    return this.json;
  }

  static fromText(o: string): JSONTextSettings {
    return new JSONTextSettings(o);
  }

  static fromSettings(data: Settings): JSONTextSettings {
    let json = {
      keymaps: data.keymaps.toJSON(),
      search: data.search,
      properties: data.properties,
      blacklist: data.blacklist,
    };
    return new JSONTextSettings(JSON.stringify(json, undefined, 2));
  }
}

export class FormSettings {
  public readonly keymaps: FormKeymaps;

  public readonly search: FormSearch;

  public readonly properties: Properties;

  public readonly blacklist: Blacklist;

  constructor(
    keymaps: FormKeymaps,
    search: FormSearch,
    properties: Properties,
    blacklist: Blacklist,
  ) {
    this.keymaps = keymaps;
    this.search = search;
    this.properties = properties;
    this.blacklist = blacklist;
  }

  buildWithKeymaps(keymaps: FormKeymaps): FormSettings {
    return new FormSettings(
      keymaps,
      this.search,
      this.properties,
      this.blacklist,
    );
  }

  buildWithSearch(search: FormSearch): FormSettings {
    return new FormSettings(
      this.keymaps,
      search,
      this.properties,
      this.blacklist,
    );
  }

  buildWithProperties(props: Properties): FormSettings {
    return new FormSettings(
      this.keymaps,
      this.search,
      props,
      this.blacklist,
    );
  }

  buildWithBlacklist(blacklist: Blacklist): FormSettings {
    return new FormSettings(
      this.keymaps,
      this.search,
      this.properties,
      blacklist,
    );
  }

  toSettings(): Settings {
    return Settings.fromJSON({
      keymaps: this.keymaps.toKeymaps().toJSON(),
      search: this.search.toSearchSettings().toJSON(),
      properties: this.properties.toJSON(),
      blacklist: this.blacklist.toJSON(),
    });
  }

  toJSON(): {
    keymaps: ReturnType<FormKeymaps['toJSON']>;
    search: ReturnType<FormSearch['toJSON']>;
    properties: ReturnType<Properties['toJSON']>;
    blacklist: ReturnType<Blacklist['toJSON']>;
    } {
    return {
      keymaps: this.keymaps.toJSON(),
      search: this.search.toJSON(),
      properties: this.properties.toJSON(),
      blacklist: this.blacklist.toJSON(),
    };
  }

  static fromJSON(o: ReturnType<FormSettings['toJSON']>): FormSettings {
    for (let name of ['keymaps', 'search', 'properties', 'blacklist']) {
      if (!Object.prototype.hasOwnProperty.call(o, name)) {
        throw new Error(`"${name}" field not set`);
      }
    }
    return new FormSettings(
      FormKeymaps.fromJSON(o.keymaps),
      FormSearch.fromJSON(o.search),
      Properties.fromJSON(o.properties),
      Blacklist.fromJSON(o.blacklist),
    );
  }

  static fromSettings(data: Settings): FormSettings {
    return new FormSettings(
      FormKeymaps.fromKeymaps(data.keymaps),
      FormSearch.fromSearch(data.search),
      data.properties,
      data.blacklist);
  }
}

export enum SettingSource {
  JSON = 'json',
  Form = 'form',
}

export default class SettingData {
  private source: SettingSource;

  private json?: JSONTextSettings;

  private form?: FormSettings;

  constructor({
    source, json, form
  }: {
    source: SettingSource,
    json?: JSONTextSettings,
    form?: FormSettings,
  }) {
    this.source = source;
    this.json = json;
    this.form = form;
  }

  getSource(): SettingSource {
    return this.source;
  }

  getJSON(): JSONTextSettings {
    if (!this.json) {
      throw new TypeError('json settings not set');
    }
    return this.json;
  }

  getForm(): FormSettings {
    if (!this.form) {
      throw new TypeError('form settings not set');
    }
    return this.form;
  }

  toJSON(): any {
    switch (this.source) {
    case SettingSource.JSON:
      return {
        source: this.source,
        json: (this.json as JSONTextSettings).toJSONText(),
      };
    case SettingSource.Form:
      return {
        source: this.source,
        form: (this.form as FormSettings).toJSON(),
      };
    }
    throw new Error(`unknown settings source: ${this.source}`);
  }

  toSettings(): Settings {
    switch (this.source) {
    case SettingSource.JSON:
      return this.getJSON().toSettings();
    case SettingSource.Form:
      return this.getForm().toSettings();
    }
    throw new Error(`unknown settings source: ${this.source}`);
  }

  static fromJSON(o: {
    source: string;
    json?: string;
    form?: ReturnType<FormSettings['toJSON']>;
  }): SettingData {
    switch (o.source) {
    case SettingSource.JSON:
      return new SettingData({
        source: o.source,
        json: JSONTextSettings.fromText(
          o.json as ReturnType<JSONTextSettings['toJSONText']>),
      });
    case SettingSource.Form:
      return new SettingData({
        source: o.source,
        form: FormSettings.fromJSON(
          o.form as ReturnType<FormSettings['toJSON']>),
      });
    }
    throw new Error(`unknown settings source: ${o.source}`);
  }
}

export const DefaultSettingData: SettingData = SettingData.fromJSON({
  source: 'json',
  json: DefaultSettingJSONText,
});