import { PublishResponse } from "@base/networking/PublishResponse";
import { Editor, EditorPosition, parseYaml, stringifyYaml } from "obsidian";

export const YAML_REGEX = /^---\n(?:((?:.|\n)*?)\n)?---(?=\n|$)/;

type CommonUpdateParam = {
  editor: Editor;
  key: string;
  value: string;
  action: 'replace' | 'insert' | 'remove';
}

type BulkUpdateParam = {
  editor: Editor;
  updateDatas: Record<string, unknown>;
  removeDatas: string[];
  action: 'bulk'
}

export interface FrontmatterServiceInterface {
  updateFrontmatter(response: PublishResponse, editor: Editor): void
}

export interface FrontmatterServiceDelegate {
  frontmatterUpdateDidSucceed(): void

  frontmatterUpdateDidFail(error: Error): void
}

export class FrontmatterService implements FrontmatterServiceInterface {

  // Properties

  private delegate?: FrontmatterServiceDelegate

  // Life cycle

  constructor(delegate?: FrontmatterServiceDelegate) {
    this.delegate = delegate
  }

  // Public

  public async updateFrontmatter(response: PublishResponse, editor: Editor) {
    const yaml = this.getObjectYaml(editor)

    this.upsert("url", response.url, false, editor)
    this.delegate?.frontmatterUpdateDidSucceed()
  }


  // Get objectify yaml of current file
  getObjectYaml(editor: Editor) {
    const stringYaml = this.getYaml(editor);

    return stringYaml? parseYaml(stringYaml.slice(4, -4)): {}
  }

  // Exchange item position in array
  itemMove<T>(arr: T[], itemIdx1: number, itemIdx2: number): void {
    [arr[itemIdx1], arr[itemIdx2]] = [arr[itemIdx2], arr[itemIdx1]];
  }

  // Add item in specific position
  itemAdd<T>(arr: T[], itemIdx: number, item: T): void {
    arr.splice(itemIdx, 0, item)
  }

  // Delete specific item in array
  itemDelete<T>(arr: T[], itemIndex: number): void {
    arr.splice(itemIndex, 1);
  }

  // Get yaml section
  getYaml(editor: Editor): string {
    const matchResult = editor.getValue().match(YAML_REGEX);
    
    return matchResult?.[0] ?? '';
  } 

  generateActionKeyword(data: CommonUpdateParam | BulkUpdateParam) {
    const {editor, action} = data;
    const yamlSection = this.getYaml(editor);
    const yaml = yamlSection.slice(4, -3);
    const objectYaml = this.getObjectYaml(editor);
    const objectSnippet: Record<string, unknown> = {};

    if (action === 'replace') objectSnippet[data.key] = data.value;

    if (action === 'insert') {
      if (objectYaml[data.key] instanceof Array) {
        console.log('inserting into array')
        objectSnippet[data.key] = [...objectYaml[data.key], data.value]; 
      } else {
        console.log('inserting')
        //objectSnippet[data.key] = [data.value];
        objectSnippet[data.key] = data.value;
      }
    }
    if (action === 'remove') {
      if (objectYaml[data.key] instanceof Array) {
        const newValue = objectYaml[data.key].filter((val: string) => val !== data.value);
        objectSnippet[data.key] = newValue.length? newValue: null;
      } else {
        objectSnippet[data.key] = null;
      }
    }

    if (action === 'bulk') {
      Object.entries(data.updateDatas).forEach(([key, value]) => objectSnippet[key] = value );
      data.removeDatas.forEach((key) => objectSnippet[key] = null)
    }

    let replacement = `---\n${this.generateReplacement(yaml, objectSnippet)}---`;
    const startPosition: EditorPosition = {line: 0, ch: 0};
    const endPosition: EditorPosition = editor.offsetToPos(yamlSection.length);

    // Make sure there is at least one newline character
    // after the end of the frontmatter
    const charAfterYaml = editor.getRange(endPosition, {ch: endPosition.ch + 1, line: endPosition.line})
    replacement = charAfterYaml == "\n" ? replacement : replacement + "\n"

    return {replacement, startPosition, endPosition}
  }

  generateReplacement(yaml: string, snippet: Record<string, unknown>) {
    return Object.entries(snippet).reduce((temp, [key, value]) => {
      const YAML_FIELD_REGEX = new RegExp(`(${key} *:).+?\\n(?=\\S|$)`, 'gs');
    
      const replacement = (value === null)? '': stringifyYaml({[key]: value});
    
      return temp.match(YAML_FIELD_REGEX)? temp.replace(YAML_FIELD_REGEX, replacement): `${temp}${replacement}`;
    }, yaml)
  }

  flatYamlFields(yaml: string, flatFields: string[]): string {
    const objectYaml = parseYaml(yaml.slice(4, -4));

    return flatFields.reduce((temp, key) => {
      const YAML_FIELD_REGEX = new RegExp(`(${key}:).+?(?=\\n\\S|$)`, 'gs');

      return temp.match(YAML_FIELD_REGEX)? temp.replace(YAML_FIELD_REGEX, `$1 [${objectYaml[key].join(', ')}]`): temp;
    }, yaml)
  }

  replace(key: string, value: string, editor: Editor): void {
    const {replacement, startPosition, endPosition} = this.generateActionKeyword({key, value, editor, action: 'replace'});

    editor.replaceRange(replacement, startPosition, endPosition)
  }

  insert(key: string, value: string, flat: boolean, editor: Editor): void {
    const {replacement, startPosition, endPosition} = this.generateActionKeyword({key, value, editor, action: 'insert'});

    const postProcessedReplacement = flat? this.flatYamlFields(replacement, [key]): replacement;

    editor.replaceRange(postProcessedReplacement, startPosition, endPosition)
  }

  upsert(key: string, value: string, flat: boolean, editor: Editor): void {
    const yaml = this.getObjectYaml(editor)

    if (yaml[key]) {
      this.replace(key, value, editor)
    } else {
      this.insert(key, value, flat, editor)
    }
  }

  remove(key: string, value: string, flat: boolean, editor: Editor): void {
    const {replacement, startPosition, endPosition} = this.generateActionKeyword({key, value, editor, action: 'remove'});

    const postProcessedReplacement = flat? this.flatYamlFields(replacement, [key]): replacement;

    editor.replaceRange(postProcessedReplacement, startPosition, endPosition)
  }

  bulkUpdate(updateDatas: Record<string, unknown>, removeDatas: string[], flatFields: string[], editor: Editor): void {
    const {replacement, startPosition, endPosition} = this.generateActionKeyword({updateDatas, removeDatas, editor, action: 'bulk'});
    
    const flattedReplacement = this.flatYamlFields(replacement, flatFields);

    editor.replaceRange(flattedReplacement, startPosition, endPosition);
  }
}