import { Component, Input, ViewChild } from '@angular/core';
import { CodeExecutionService } from 'src/app/services/codeExecution.service';
import { Compartment, StateEffect } from '@codemirror/state';
import { CodeMirrorComponent } from '@sandkasten/codemirror6-editor';
import { LanguageDescription } from '@codemirror/language';
import { CODE_DEFAULTS, LANGUAGES } from '../languages';
import { SafeHTMLPipe } from '../../safe-html.pipe';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import {
  keymap,
  highlightSpecialChars,
  drawSelection,
  highlightActiveLine,
  dropCursor,
  rectangularSelection,
  crosshairCursor,
  lineNumbers,
  highlightActiveLineGutter,
  gutter,
} from '@codemirror/view';
import { Extension, EditorState } from '@codemirror/state';
import {
  defaultHighlightStyle,
  syntaxHighlighting,
  indentOnInput,
  bracketMatching,
  foldGutter,
  foldKeymap,
} from '@codemirror/language';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
import {
  autocompletion,
  completionKeymap,
  closeBrackets,
  closeBracketsKeymap,
} from '@codemirror/autocomplete';
import { lintKeymap } from '@codemirror/lint';

const basicSetup: Extension = (() => [
  highlightActiveLineGutter(),
  highlightSpecialChars(),
  history(),
  foldGutter(),
  drawSelection(),
  dropCursor(),
  EditorState.allowMultipleSelections.of(true),
  indentOnInput(),
  syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
  bracketMatching(),
  closeBrackets(),
  autocompletion(),
  rectangularSelection(),
  crosshairCursor(),
  highlightActiveLine(),
  highlightSelectionMatches(),
  keymap.of([
    ...closeBracketsKeymap,
    ...defaultKeymap,
    ...searchKeymap,
    ...historyKeymap,
    ...foldKeymap,
    ...completionKeymap,
    ...lintKeymap,
  ]),
])();

@Component({
  selector: 'app-editor-live',
  templateUrl: './editor-live.component.html',
  styleUrls: ['./editor-live.component.scss'],
  standalone: true,
  imports: [
    CodeMirrorComponent,
    ReactiveFormsModule,
    FormsModule,
    SafeHTMLPipe,
  ],
})
export class EditorLiveComponent {
  isLoaded: boolean = false; // Pour vérifier si le chargement est terminé


  readonly languages: LanguageDescription[] = LANGUAGES;
  // Mode par défaut
  private _selectedLanguage = this.languages.find(
    (lang) => lang.name === 'JavaScript'
  )!;
  get selectedLanguage(): LanguageDescription {
    return this._selectedLanguage;
  }
  set selectedLanguage(value: LanguageDescription) {
    this._selectedLanguage = value;
    if (value.name in CODE_DEFAULTS) {
      this.editorContent =
        CODE_DEFAULTS[value.name as keyof typeof CODE_DEFAULTS];
    }
    this.selectedLanguage.load().then((language) => {
      this.codemirror.editor?.dispatch({
        effects: this.languageCompartment.reconfigure(language),
      });
    });
  }
  private _linesNumbers: boolean = true;
  get linesNumbers() {
    return this._linesNumbers;
  }
  set linesNumbers(lines: boolean) {
    this._linesNumbers = lines;
    this.codemirror.editor?.dispatch({
      effects: this.gutterCompartment.reconfigure(
        lines ? lineNumbers() : gutter({})
      ),
    });
  }

  // Contenu de l'éditeur que l'on passera au serveur
  editorContent: string =
    CODE_DEFAULTS[this.selectedLanguage.name as keyof typeof CODE_DEFAULTS];
  resultContent: string = '';

  // Message d'erreur
  errorMessage: string = '';

  @ViewChild(CodeMirrorComponent) private codemirror!: CodeMirrorComponent;

  private readonly languageCompartment = new Compartment();
  private readonly gutterCompartment = new Compartment();
  protected readonly extensions: Extension[] = [
    basicSetup,
    this.gutterCompartment.of(lineNumbers()),
    this.languageCompartment.of(this.selectedLanguage.support!),
  ];

  private client: WebSocket | undefined;
  @Input()
  set idRoom(idRoom: string) {
    this.client = new WebSocket(`ws://127.0.0.1:3000/live/${idRoom}`);
    this.client.addEventListener('open', async () => {
      let conn = new Connection(this.client!);
      console.log('open')
      let {version, doc} = await getDocument(conn);
      console.log('res')
      this.codemirror.editor?.dispatch({
        changes: {
          from: 0,
          to: this.codemirror.editor.state.doc.length,
          insert: doc
        }
      })
      this.codemirror.editor?.dispatch({
        effects: StateEffect.appendConfig.of([peerExtension(version, conn)]),
      })
    });
  }


  constructor(private codeExecutionService: CodeExecutionService) {}

  // Efface le contenu de l'éditeur
  clear(): void {
    this.editorContent = '';
  }

  onRunButtonClicked() {
    // Le code à exécuter est le contenu de l'éditeur
    const codeToExecute = this.editorContent;
    this.codeExecutionService.executeCode(
      codeToExecute,
      this.selectedLanguage.name
    );

    this.resultContent = '';
  }

  loadFromFile(event: Event) {
    const file = (event.target as HTMLInputElement).files![0];
    for (const language of this.languages) {
      if (language.extensions.some((ext) => file.name.endsWith(`.${ext}`))) {
        this.selectedLanguage = language;
        const reader = new FileReader();
        reader.onload = (event) => {
          this.editorContent = event.target!.result as string;
          this.errorMessage = '';
        };
        reader.readAsText(file);
        return;
      }
    }
    const extensions = this.languages.flatMap((lang) => lang.extensions);
    this.errorMessage = `Unsupported language. Please select one of the following languages: ${extensions.join(', ')}.`;
    console.error(this.errorMessage);
  }

  saveToFile() {
    const blob = new Blob([this.editorContent], { type: 'text/plain' });
    const a = document.createElement('a');
    a.download = `code.${this.selectedLanguage.extensions![0]}`;
    a.href = URL.createObjectURL(blob);
    a.click();
  }
}

/*live*/
import { ChangeSet, Text } from '@codemirror/state';
import {EditorView} from "codemirror"
import { Update, collab, getSyncedVersion, receiveUpdates, sendableUpdates } from '@codemirror/collab';
import { ViewPlugin, ViewUpdate } from '@codemirror/view';

class Connection {
  private requestId = 0;
  private resolves: Record<number, (value: any) => void> = {};

  constructor(private client: WebSocket) {
    client.addEventListener('message', (event) => {
      const response = JSON.parse(event.data);
      if ('_request' in response) {
        const resolve = this.resolves[response._request];
        if (resolve) {
          resolve(response.payload);
        } else {
          console.error('Received response for unknown or already used request', response._request);
        }
      } else {
        console.error('Received invalid response', response._request);
      }
    })
  }

  request(body: Record<string, unknown>): Promise<any> {
    body['_request'] = this.requestId;
    this.client.send(JSON.stringify(body));
    return new Promise((resolve) => this.resolves[this.requestId++] = resolve);
  }
}

function pushUpdates(
  connection: Connection,
  version: number,
  fullUpdates: readonly Update[]
): Promise<boolean> {
  // Strip off transaction data
  let updates = fullUpdates.map(u => ({
    clientID: u.clientID,
    changes: u.changes.toJSON()
  }));
  console.log("test1");
  return connection.request({type: "pushUpdates", version, updates});
}

function pullUpdates(
  connection: Connection,
  version: number
): Promise<readonly Update[]> {
  console.log("test2");

  return connection.request({type: "pullUpdates", version})
    .then(updates => updates.map((u: any) => ({
      changes: ChangeSet.fromJSON(u.changes),
      clientID: u.clientID
    })));
}

function getDocument(
  connection: Connection
): Promise<{version: number, doc: Text}> {
  console.log("test3");

  return connection.request({ type: "getDocument" }).then(data => ({
    version: data.version,
    doc: Text.of(data.doc.split("\n"))
  }));
}

function peerExtension(startVersion: number, connection: Connection) {
  console.log(connection)
  let plugin = ViewPlugin.fromClass(class {
    private pushing = false
    private done = false

    constructor(private view: EditorView) { this.pull() }

    update(update: ViewUpdate) {
      if (update.docChanged) this.push()
    }

    async push() {
      let updates = sendableUpdates(this.view.state)
      console.log("push");
      if (this.pushing || !updates.length) return
      this.pushing = true
      let version = getSyncedVersion(this.view.state)
      await pushUpdates(connection, version, updates)
      this.pushing = false
      console.log("push2");

      // Regardless of whether the push failed or new updates came in
      // while it was running, try again if there's updates remaining
      if (sendableUpdates(this.view.state).length)
        setTimeout(() => this.push(), 100)
    }

    async pull() {
      while (!this.done) {
        let version = getSyncedVersion(this.view.state)
        let updates = await pullUpdates(connection, version)
        console.log(updates)
        this.view.dispatch(receiveUpdates(this.view.state, updates))
      }
    }

    destroy() { this.done = true }
  })
  return [collab({startVersion}), plugin]
}

async function createPeer(connection: Connection) {
  let {version, doc} = await getDocument(connection)
  console.log(doc);
  let state = EditorState.create({
    doc,
    extensions: [basicSetup, peerExtension(version, connection)],
  })
  return new EditorView({state, parent: document.body})
}

//const client = new WebSocket('ws://127.0.0.1:3000/live/test');
//client.addEventListener('open', () => createPeer(new Connection(client)));