import * as d3 from 'd3';

interface Grammar {
  abstract: AbstractGrammar;
  concretes: {
    [key: string]: ConcreteGrammar;
  };
}

interface AbstractGrammar {
  name: string;
  startcat: string;
  funs: {
    [key: string]: {
      args: string[];
      cat: string;
    };
  };
}

export interface GrammarNode {
  name: string;
  children?: GrammarNode[];
  type: 'cat' | 'fun';
  funs?: string[];
  originalName?: string;
  concreteFunctions?: {
    [key: string]: string[];
  };
}

interface Production {
  type: string;
  fid: number;
  args: Arg[];
}

interface Arg {
type: string;
  hypos: any[];
  fid: number;
}

interface ConcreteFunction {
  name: string;
  lins: number[];
}

type Sequence = SymCat | SymKS | SymLit;

interface SymCat {
  type: 'SymCat';
  args: number[];
}

interface SymKS {
  type: 'SymKS';
  args: string[];
}

interface SymLit {
  type: 'SymLit';
  args: number[];
}

interface AbstractGrammar {
  name: string;
  startcat: string;
  funs: {
    [key: string]: {
      args: string[];
      cat: string;
    };
  };
}

export interface ConcreteGrammar {
  flags: {
    language: string;
  };

  productions: {
    [key: string]: Production[];
  };

  functions: ConcreteFunction[];

  sequences: Sequence[][];

  categories: {
    [key: string]: {
      start: number;
      end: number;
    };
  };

  totalfids: number;
}

interface ConcreteFunctionWithLin extends ConcreteFunction {
  resolvedLins?: string[];
}

interface ConcreteGrammarWithLin extends ConcreteGrammar {
  functions: ConcreteFunctionWithLin[];
  resolvedSequences: string[];
}

interface GrammarWithLin extends Grammar {
  concretes: {
    [key: string]: ConcreteGrammarWithLin;
  };
}


//

export class GFD3 {
  private grammar: Grammar | null = null;
  private abstractAST: GrammarNode | null = null;
  private grammarMode: 'abstract' | 'concrete' = 'abstract';
  private selectedConcrete: string | null = null;

  async loadGrammarFromFile(file: File): Promise<void> {
    const text = await file.text();
    this.setGrammar(JSON.parse(text));
  }

  async loadGrammarFromURL(url: string): Promise<void> {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const json = await response.json();
    this.setGrammar(json);
  }

  private setGrammar(grammar: Grammar): void {
    this.grammar = grammar;
    this.abstractAST = this.transformAbstractToTree(grammar);
    this.grammarMode = 'abstract';
    const concreteLanguages = this.getConcreteLanguages();
    if (concreteLanguages.length > 0) {
      this.selectedConcrete = concreteLanguages[0];
    }
  }

  private transformAbstractToTree(grammar: Grammar): GrammarNode {
    const startCat = grammar.abstract.startcat;

    const buildTree = (cat: string, visited: Set<string>): GrammarNode => {
      if (visited.has(cat)) {
        return { name: cat, type: 'cat' };
      }
    
      visited.add(cat);
      const node: GrammarNode = { name: cat, children: [], type: 'cat' };
      
      const funs = Object.entries(grammar.abstract.funs)
        .filter(([, funDetails]) => funDetails.cat === cat)
        .map(([funName]) => funName);
      
      node.funs = funs;
    
      if (grammar.concretes) {
        node.concreteFunctions = {};
        Object.entries(grammar.concretes).forEach(([lang, concrete]) => {
          if (concrete.productions[cat]) {
            node.concreteFunctions![lang] = concrete.productions[cat]
              .map(prod => concrete.functions[prod.fid].name);
          }
        });
      }
    
      Object.values(grammar.abstract.funs)
        .filter(fun => fun.cat === cat)
        .forEach(fun => {
          fun.args.forEach(arg => {
            if (!node.children?.some(child => child.name === arg)) {
              node.children?.push(buildTree(arg, new Set(visited)));
            }
          });
        });
    
      visited.delete(cat);
      return node;
    };

    return buildTree(startCat, new Set<string>());
  }

  getGrammar(): Grammar | null {
    return this.grammar;
  }

  getAbstractAST(): GrammarNode | null {
    return this.abstractAST;
  }

  getConcreteLanguages(): string[] {
    if (!this.grammar || !this.grammar.concretes) return [];
    return Object.values(this.grammar.concretes).map(concrete => concrete.flags.language);
  }

  setGrammarMode(mode: 'abstract' | 'concrete'): void {
    this.grammarMode = mode;
  }

  getGrammarMode(): 'abstract' | 'concrete' {
    return this.grammarMode;
  }

  setSelectedConcrete(language: string): void {
    this.selectedConcrete = language;
  }

  getSelectedConcrete(): string | null {
    return this.selectedConcrete;
  }

  getOptionsForNode(node: GrammarNode): string[] {
    if (!this.grammar) return [];

    const category = node.name;

    if (this.grammarMode === 'abstract') {
      return Object.entries(this.grammar.abstract.funs)
        .filter(([, funDetails]) => funDetails.cat === category)
        .map(([funName]) => funName);
    } else if (this.selectedConcrete) {
      const concreteLang = Object.keys(this.grammar.concretes).find(
        key => this.grammar!.concretes[key].flags.language === this.selectedConcrete
      );
      if (concreteLang) {
        const concrete = this.grammar.concretes[concreteLang];
        if (concrete.productions[category]) {
          return concrete.productions[category]
            .map(prod => concrete.functions[prod.fid].name);
        }
      }
    }

    return [];
  }

  updateNodeName(node: GrammarNode, newName: string): void {
    node.originalName = node.originalName || node.name;
    node.name = newName;
  }

  resetNodeName(node: GrammarNode): void {
    if (node.originalName) {
      node.name = node.originalName;
    }
  }

  parseLins(lins: number[], sequences: Sequence[][]): string {
    return lins.map(linIndex => {
      const sequence = sequences[linIndex];
      return sequence.map(seq => {
        if (seq.type === 'SymKS') {
          return seq.args[0];
        }
        if (seq.type === 'SymCat') {
          return `{${seq.args[1]}}`;
        }
        if (seq.type === 'SymLit') {
          return seq.args.join('');
        }
        return '';
      }).join(' ');
    }).join(' ');
  }

  resolveSequence(sequence: Sequence[], cats: string[]): string {
    return sequence.map(seq => {
      if (seq.type === 'SymKS') {
        return seq.args[0];
      }
      if (seq.type === 'SymCat') {
        const catIndex = seq.args[0];
        return cats[catIndex] || `{${catIndex}}`;
      }
      if (seq.type === 'SymLit') {
        return `<${seq.args.join(',')}>`;
      }
      return '';
    }).join(' ');
  }

  replaceLins(): GrammarWithLin {
    if (!this.grammar) throw new Error("Grammar not loaded");

    const resolvedGrammar: GrammarWithLin = {
      ...this.grammar,
      concretes: {}
    };
  
    const cats = new Set<string>();
    Object.values(this.grammar.abstract.funs).forEach(fun => {
      cats.add(fun.cat);
      fun.args.forEach(arg => cats.add(arg));
    });
    const catsArray = Array.from(cats);
  
    for (const [concreteName, concreteGrammar] of Object.entries(this.grammar.concretes)) {
      const resolvedFunctions: ConcreteFunctionWithLin[] = concreteGrammar.functions.map(func => ({
        ...func,
        resolvedLins: func.lins.map(linIndex => 
          this.resolveSequence(concreteGrammar.sequences[linIndex], catsArray)
        )
      }));
  
      resolvedGrammar.concretes[concreteName] = {
        ...concreteGrammar,
        functions: resolvedFunctions,
        resolvedSequences: concreteGrammar.sequences.map(seq => 
          this.resolveSequence(seq, catsArray)
        )
      };
    }
  
    return resolvedGrammar;
  }

  // return data
  getTreeData(): d3.HierarchyNode<GrammarNode> | null {
    if (!this.abstractAST) return null;
    return d3.hierarchy(this.abstractAST);
  }
}