// CodeMirror 6 Logtalk language support
// Copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE

// Parts from Ace; see <https://raw.githubusercontent.com/ajaxorg/ace/master/LICENSE>
// Author: Paulo Moura

import { StreamLanguage, indentOnInput, indentUnit, foldService } from "@codemirror/language"
import { indentWithTab } from '@codemirror/commands';
import { keymap } from '@codemirror/view';
import { Prec } from '@codemirror/state';

// Logtalk stream parser for CodeMirror 6
export const logtalk = StreamLanguage.define({

  startState() {
    return {
      level: 0,
      inComment: false,
      inString: false,
      stringDelim: null,
      stringType: null  // 'atom' for quoted atoms, 'string' for double-quoted terms
    };
  },

  token(stream, state) {
    // Handle block comments
    if (state.inComment) {
      if (stream.match(/\*\//)) {
        state.inComment = false;
        return "comment";
      }
      stream.next();
      return "comment";
    }

    // Handle strings and quoted atoms
    if (state.inString) {
      if (stream.match(state.stringDelim)) {
        state.inString = false;
        const returnType = state.stringType === 'atom' ? 'atom' : 'string';
        state.stringDelim = null;
        state.stringType = null;
        return returnType;
      }

      // Control character escape sequences
      if (stream.match(/\\[abfnrtv\\'"]/)) {
        // Standard escape sequences: \a \b \f \n \r \t \v \\ \' \"
        return "string.escape";
      }
      if (stream.match(/\\[0-7]+\\/)) {
        // Octal escape sequences: \123\
        return "string.escape";
      }
      if (stream.match(/\\x[0-9a-fA-F]+\\/)) {
        // Hexadecimal escape sequences: \x1F\
        return "string.escape";
      }
      if (stream.match(/\\u[0-9a-fA-F]{4}/)) {
        // Unicode escape sequences: \u1234
        return "string.escape";
      }
      if (stream.match(/\\U[0-9a-fA-F]{8}/)) {
        // Extended Unicode escape sequences: \U12345678
        return "string.escape";
      }
      if (stream.match(/\\\s/)) {
        // Line continuation (backslash followed by whitespace)
        return "string.escape";
      }

      stream.next();
      return state.stringType === 'atom' ? 'atom' : 'string';
    }

    // Start of block comment
    if (stream.match(/\/\*/)) {
      state.inComment = true;
      return "comment";
    }

    // Line comment
    if (stream.match(/%.*$/)) {
      return "comment";
    }

    // Quoted atom
    if (stream.match(/'/)) {
      state.inString = true;
      state.stringDelim = "'";
      state.stringType = 'atom';
      return "atom";
    }

    // Double-quoted term
    if (stream.match(/"/)) {
      state.inString = true;
      state.stringDelim = '"';
      state.stringType = 'string';
      return "string";
    }

    // Entity opening directives
    if (stream.match(/:-\s(?:object|protocol|category|module)(?=\()/)) {
      return "meta";
    }

    // End entity directives
    if (stream.match(/:-\send_(?:object|protocol|category)(?=\.)/)) {
      return "meta";
    }

    // Entity relations
    if (stream.match(/\b(?:complements|extends|instantiates|imports|implements|specializes)(?=\()/)) {
      return "meta";
    }

    // Other directives
    if (stream.match(/:-\s(?:else|endif|built_in|dynamic|synchronized|threaded)(?=\.)/)) {
      return "meta";
    }
    if (stream.match(/:-\s(?:calls|coinductive|elif|encoding|ensure_loaded|export|if|include|initialization|info|reexport|set_(?:logtalk|prolog)_flag|uses)(?=\()/)) {
      return "meta";
    }
    if (stream.match(/:-\s(?:alias|info|dynamic|discontiguous|meta_(?:non_terminal|predicate)|mode|multifile|public|protected|private|op|uses|use_module|synchronized)(?=\()/)) {
      return "meta";
    }

    // Message sending operator
    if (stream.match(/::/)) {
      return "operator";
    }

    // Explicit module qualification
    if (stream.match(/:/)) {
      return "operator";
    }

    // External call operators
    if (stream.match(/[{}]/)) {
      return "operator";
    }

    // Mode operators
    if (stream.match(/[?@]/)) {
      return "operator";
    }

    // Comparison operators
    if (stream.match(/@(?:=<|<|>|>=)|==|\\==/)) {
      return "operator";
    }
    if (stream.match(/=<|[<>]=?|=:=|=\\=/)) {
      return "operator";
    }

    // Bitwise operators
    if (stream.match(/<<|>>|\/\\|\\\/|\\/)) {
      return "operator";
    }

    // Arithmetic operators
    if (stream.match(/\*\*|[+\-*\/]|\/\//)) {
      return "operator";
    }

    // Evaluable functions
    if (stream.match(/\b(?:e|pi|div|mod|rem)\b(?![_!(^~])/)) {
      return "operator";
    }

    // Misc operators
    if (stream.match(/:-|!|\\+|[,;]|-->|->|=|\\=|\.|\.\.|\^|\bas\b|\bis\b/)) {
      return "operator";
    }

    // Built-in predicates - evaluable functions
    if (stream.match(/\b(?:abs|acos|asin|atan|atan2|ceiling|cos|div|exp|float(?:_(?:integer|fractional)_part)?|floor|log|max|min|mod|rem|round|sign|sin|sqrt|tan|truncate|xor)(?=\()/)) {
      return "builtin";
    }

    // Control predicates
    if (stream.match(/\b(?:true|fail|false|repeat|(?:instantiation|system)_error)\b(?![_!(^~])/)) {
      return "builtin";
    }
    if (stream.match(/\b(?:uninstantiation|type|domain|consistency|existence|permission|representation|evaluation|resource|syntax)_error(?=\()/)) {
      return "builtin";
    }
    if (stream.match(/\b(?:call|catch|ignore|throw|once)(?=\()/)) {
      return "builtin";
    }

    // Event handlers
    if (stream.match(/\b(after|before)(?=\()/)) {
      return "builtin";
    }

    // Message forwarding handler
    if (stream.match(/\bforward(?=\()/)) {
      return "builtin";
    }
    // Execution-context methods
    if (stream.match(/\b(context|parameter|this|se(lf|nder))(?=\()/)) {
      return "builtin";
    }
    // Reflection
    if (stream.match(/\b(current_predicate|predicate_property)(?=\()/)) {
      return "builtin";
    }
    // DCGs and term expansion
    if (stream.match(/\b(expand_(goal|term)|(goal|term)_expansion|phrase)(?=\()/)) {
      return "builtin";
    }

    // Entity creation and destruction
    if (stream.match(/\b(abolish|c(reate|urrent))_(object|protocol|category)(?=\()/)) {
      return "builtin";
    }

    // Entity properties
    if (stream.match(/\b(object|protocol|category)_property(?=\()/)) {
      return "builtin";
    }

    // Entity relations
    if (stream.match(/\bco(mplements_object|nforms_to_protocol)(?=\()/)) {
      return "builtin";
    }
    if (stream.match(/\bextends_(object|protocol|category)(?=\()/)) {
      return "builtin";
    }
    if (stream.match(/\bimp(lements_protocol|orts_category)(?=\()/)) {
      return "builtin";
    }
    if (stream.match(/\b(instantiat|specializ)es_class(?=\()/)) {
      return "builtin";
    }

    // Events
    if (stream.match(/\b(current_event|(abolish|define)_events)(?=\()/)) {
      return "builtin";
    }

    // Flags
    if (stream.match(/\b(create|current|set)_logtalk_flag(?=\()/)) {
      return "builtin";
    }

    // Compiling, loading, and library paths
    if (stream.match(/\blogtalk_(compile|l(ibrary_path|oad|oad_context)|make(_target_action)?)(?=\()/)) {
      return "builtin";
    }
    if (stream.match(/\blogtalk_make\b/)) {
      return "builtin";
    }

    // Database
    if (stream.match(/\b(clause|retract(all)?)(?=\()/)) {
      return "builtin";
    }
    if (stream.match(/\ba(bolish|ssert(a|z))(?=\()/)) {
      return "builtin";
    }

    // All solutions
    if (stream.match(/\b((bag|set)of|f(ind|or)all)(?=\()/)) {
      return "builtin";
    }

    // Multi-threading predicates
    if (stream.match(/\bthreaded(_(ca(ll|ncel)|once|ignore|exit|peek|wait|notify))?(?=\()/)) {
      return "builtin";
    }

    // Engine predicates
    if (stream.match(/\bthreaded_engine(_(create|destroy|self|next|next_reified|yield|post|fetch))?(?=\()/)) {
      return "builtin";
    }

    // Term unification
    if (stream.match(/\b(subsumes_term|unify_with_occurs_check)(?=\()/)) {
      return "builtin";
    }

    // Term creation and decomposition
    if (stream.match(/\b(functor|arg|copy_term|numbervars|term_variables)(?=\()/)) {
      return "builtin";
    }

    // Stream selection and control
    if (stream.match(/\b(curren|se)t_(in|out)put(?=\()/)) {
      return "builtin";
    }
    if (stream.match(/\b(open|close)(?=[(])(?=\()/)) {
      return "builtin";
    }
    if (stream.match(/\bflush_output(?=[(])(?=\()/)) {
      return "builtin";
    }
    if (stream.match(/\b(at_end_of_stream|flush_output)\b/)) {
      return "builtin";
    }
    if (stream.match(/\b(stream_property|at_end_of_stream|set_stream_position)(?=\()/)) {
      return "builtin";
    }

    // Character and byte input/output
    if (stream.match(/\b(?:(?:get|peek|put)_(?:char|code|byte)|nl)(?=\()/)) {
      return "builtin";
    }
    if (stream.match(/\bnl\b/)) {
      return "builtin";
    }

    // Term input/output
    if (stream.match(/\bread(_term)?(?=\()/)) {
      return "builtin";
    }
    if (stream.match(/\bwrite(q|_(canonical|term))?(?=\()/)) {
      return "builtin";
    }
      if (stream.match(/\b(current_)?op(?=\()/)) {
      return "builtin";
    }
    if (stream.match(/\b(current_)?char_conversion(?=\()/)) {
      return "builtin";
    }

    // Atom/term processing
    if (stream.match(/\b(?:atom_(?:length|chars|concat|codes)|sub_atom|char_code|number_(?:char|code)s)(?=\()/)) {
      return "builtin";
    }

    // Term testing
    if (stream.match(/\b(?:var|atom(ic)?|integer|float|callable|compound|nonvar|number|ground|acyclic_term)(?=\()/)) {
      return "builtin";
    }

    // Term comparison
    if (stream.match(/\bcompare(?=\()/)) {
      return "builtin";
    }

    // Sorting
    if (stream.match(/\b(key)?sort(?=\()/)) {
      return "builtin";
    }

    // Implementation defined hooks functions
    if (stream.match(/\b(se|curren)t_prolog_flag(?=\()/)) {
      return "builtin";
    }
    if (stream.match(/\bhalt\b/)) {
      return "builtin";
    }
    if (stream.match(/\bhalt(?=\()/)) {
      return "builtin";
    }

    // Numbers
    if (stream.match(/\b(?:0b[01]+|0o[0-7]+|0x[0-9a-fA-F]+)\b/)) {
      return "number";
    }
    if (stream.match(/(?<=^|\s)0'(?:\\.|.)/)) {
      return "number";
    }
    if (stream.match(/\b\d+\.?\d*(?:[eE][+-]?\d+)?\b/)) {
      return "number";
    }

    // Variables
    if (stream.match(/\b[A-Z_][A-Za-z0-9_]*\b/)) {
      return "variable";
    }

    // Skip atoms that aren't keywords or builtins
    if (stream.match(/\b[a-z][A-Za-z0-9_]*\b/)) {
      return null;
    }

    // Skip whitespace
    if (stream.match(/\s+/)) {
      return null;
    }

    // Default: consume one character
    stream.next();
    return null;
  },

  languageData: {
    name: "logtalk",
    extensions: [".lgt", ".logtalk"],
    commentTokens: { line: "%", block: { open: "/*", close: "*/" } },
    closeBrackets: { brackets: ["(", "[", "{", "'", '"'] },
    indentOnInput: /(?:^:-\s(?:object|protocol|category|module)\(.*$|^:-\send_(?:object|protocol|category)\.$|\s*.*\s:-$|.*\.$)/
  }
})

// Custom Enter key handler that uses our indentation service
function logtalkEnterHandler(view) {
  //console.log('=== Custom Enter handler called ===');

  const { state } = view;
  const pos = state.selection.main.head;
  const doc = state.doc;
  const line = doc.lineAt(pos);

  // Calculate indentation for the new line by looking at current line
  let newIndent = '';
  const tabSize = state.tabSize || 4;
  const currentIndentColumn = line.text.match(/^(\t*)/)?.[1]?.length || 0;

  // Check if current line should cause indentation or de-indentation
  if (/^:-\s(?:object|protocol|category|module)\(/.test(line.text.trim())) {
    newIndent = '\t'.repeat(currentIndentColumn + 1);
    //console.log('Indenting after entity opening directive');
  } else if (/:-\s*$/.test(line.text.trim()) || /:-(?![^(]*\)).*[^.]$/.test(line.text.trim())) {
    newIndent = '\t'.repeat(currentIndentColumn + 1);
    //console.log('Indenting after clause neck operator');
  } else if (/[(\[{]\s*$/.test(line.text)) {
    newIndent = '\t'.repeat(currentIndentColumn + 1);
    //console.log('Indenting after opening bracket');
  } else if (/.*\.$/.test(line.text.trim()) && currentIndentColumn > 0) {
    // Check if this is the end of a rule by looking for :- in previous lines
    let isEndOfRule = false;

    // Look back through previous lines to see if we're in a rule
    for (let i = line.number - 1; i >= 1; i--) {
      const prevLine = doc.line(i);
      const prevText = prevLine.text.trim();

      // If we find a period, we've reached the end of a previous clause
      if (prevText.endsWith('.')) {
        break;
      }

      // If we find :- that's not a directive, we're ending a rule
      if (/:-/.test(prevText) && !/^:-\s(?:object|protocol|category|module|end_|else|endif|built_in|dynamic|synchronized|threaded|calls|coinductive|elif|encoding|ensure_loaded|export|if|include|initialization|info|reexport|set_|uses|alias|discontiguous|meta_|mode(_non_terminal)?|multifile|public|protected|private|op|use_module)/.test(prevText)) {
        isEndOfRule = true;
        break;
      }
    }

    if (isEndOfRule) {
      // De-indent after rule termination (period at end of line)
      newIndent = '\t'.repeat(Math.max(0, currentIndentColumn - 1));
      //console.log('De-indenting after rule termination');
    } else {
      // This is a fact, maintain current indentation
      newIndent = '\t'.repeat(currentIndentColumn);
      //console.log('Maintaining indentation after fact');
    }
  } else if (/^:-\send_(?:object|protocol|category)\.$/.test(line.text.trim()) && currentIndentColumn > 0) {
    // De-indent after entity closing directives
    newIndent = '\t'.repeat(Math.max(0, currentIndentColumn - 1));
    //console.log('De-indenting after entity closing directive');
  } else {
    newIndent = '\t'.repeat(currentIndentColumn);
    //console.log('Maintaining current indentation');
  }

  //console.log('New indent:', `"${newIndent}"`);

  // Insert newline with calculated indentation
  view.dispatch({
    changes: {
      from: pos,
      insert: '\n' + newIndent
    },
    selection: { anchor: pos + 1 + newIndent.length }
  });

  return true;
}

// Logtalk code folding service
function logtalkFoldService(state, lineStart, lineEnd) {
  const doc = state.doc;
  const line = doc.lineAt(lineStart);
  const lineText = line.text.trim();

  // 1. Objects, protocols, categories - fold from opening to closing directive
  const entityOpenMatch = lineText.match(/^:-\s+(object|protocol|category|module)\s*\(/);
  if (entityOpenMatch) {
    const entityType = entityOpenMatch[1];

    // For modules, fold from opening directive to end of file
    if (entityType === 'module') {
      // Only fold if there's content after the module directive
      if (doc.lines > line.number) {
        return { from: lineEnd, to: doc.length };
      }
      return null;
    }

    // For objects, protocols, categories - find the matching end directive
    const endPattern = new RegExp(`^:-\\s+end_${entityType}\\s*\\.\\s*$`);
    for (let i = line.number + 1; i <= doc.lines; i++) {
      const nextLine = doc.line(i);
      const nextText = nextLine.text.trim();

      if (endPattern.test(nextText)) {
        // Only fold if it spans multiple lines
        if (i > line.number + 1) {
          return { from: lineEnd, to: nextLine.from };
        }
        return null;
      }
    }
  }

  // 2. Directives - fold from directive start to ending period
  const directiveMatch = lineText.match(/^:-\s+(?!end_object|end_protocol|end_category|endif|if|elif|else)([a-z_][a-z0-9_]*)\s*\(/);
  if (directiveMatch) {
    // Find the end of this directive (the period that closes it)
    let parenCount = 0;
    let inString = false;
    let stringChar = null;
    let pos = line.from;
    let endLine = line.number;

    // Start from the opening parenthesis
    const openParenPos = line.text.indexOf('(', line.text.indexOf(':-'));
    if (openParenPos !== -1) {
      pos = line.from + openParenPos;
      parenCount = 1;

      // Scan forward to find the matching closing parenthesis and period
      for (let i = pos + 1; i < doc.length; i++) {
        const char = doc.sliceString(i, i + 1);

        // Track which line we're on
        if (char === '\n') {
          endLine++;
        }

        if (inString) {
          if (char === stringChar && doc.sliceString(i - 1, i) !== '\\') {
            inString = false;
            stringChar = null;
          }
        } else {
          if (char === '"' || char === "'") {
            inString = true;
            stringChar = char;
          } else if (char === '(') {
            parenCount++;
          } else if (char === ')') {
            parenCount--;
            if (parenCount === 0) {
              // Found the closing parenthesis, now look for the period
              for (let j = i + 1; j < doc.length; j++) {
                const nextChar = doc.sliceString(j, j + 1);
                if (nextChar === '.') {
                  // Only fold if it spans multiple lines
                  if (endLine > line.number) {
                    return { from: lineEnd, to: j };
                  }
                  return null;
                } else if (!/\s/.test(nextChar)) {
                  break; // Non-whitespace character found, not a simple directive
                }
              }
              break;
            }
          }
        }
      }
    }
  }

  // 3. Clauses - fold from neck operator to ending period
  const clauseMatch = lineText.match(/^([^:]*?):-(?!\s+(object|protocol|category|module|end_|else|endif|built_in|dynamic|synchronized|threaded|calls|coinductive|elif|encoding|ensure_loaded|export|if|include|initialization|info|reexport|set_|uses|alias|discontiguous|meta_|mode|multifile|public|protected|private|op|use_module))/);
  if (clauseMatch) {
    // This is a clause (rule), find the ending period
    let pos = line.from + line.text.indexOf(':-') + 2;
    let inString = false;
    let stringChar = null;
    let parenCount = 0;
    let endLine = line.number;

    // Scan forward to find the period that ends this clause
    for (let i = pos; i < doc.length; i++) {
      const char = doc.sliceString(i, i + 1);

      // Track which line we're on
      if (char === '\n') {
        endLine++;
      }

      if (inString) {
        if (char === stringChar && doc.sliceString(i - 1, i) !== '\\') {
          inString = false;
          stringChar = null;
        }
      } else {
        if (char === '"' || char === "'") {
          inString = true;
          stringChar = char;
        } else if (char === '(' || char === '[' || char === '{') {
          parenCount++;
        } else if (char === ')' || char === ']' || char === '}') {
          parenCount--;
        } else if (char === '.' && parenCount === 0) {
          // Check if this period is at the end of a line or followed by whitespace/comment
          const nextChar = i + 1 < doc.length ? doc.sliceString(i + 1, i + 2) : '';
          if (nextChar === '' || /\s/.test(nextChar) || nextChar === '%') {
            // Only fold if it spans multiple lines
            if (endLine > line.number) {
              return { from: lineEnd, to: i };
            }
            return null;
          }
        }
      }
    }
  }

  // 4. Conditional compilation directives - if, elif, else
  const conditionalMatch = lineText.match(/^:-\s+(if|elif|else)(?:\(|\.)/);
  if (conditionalMatch) {
    const directiveType = conditionalMatch[1];

    if (directiveType === 'if') {
      // For 'if', find the corresponding 'endif' with proper nesting
      let nestingLevel = 1; // Start with 1 for the current 'if'

      for (let i = line.number + 1; i <= doc.lines; i++) {
        const nextLine = doc.line(i);
        const nextText = nextLine.text.trim();

        // Check for nested 'if' directives
        if (/^:-\s+if(?:\(|\.)/.test(nextText)) {
          nestingLevel++;
        }
        // Check for 'endif' directives
        else if (/^:-\s+endif(?:\(|\.)/.test(nextText)) {
          nestingLevel--;
          if (nestingLevel === 0) {
            // Found the matching 'endif'
            if (i > line.number) {
              return { from: lineEnd, to: nextLine.from };
            }
            return null;
          }
        }
      }

      // If no matching 'endif' found, fold to end of document
      if (doc.lines > line.number) {
        return { from: lineEnd, to: doc.length };
      }
    } else {
      // For 'elif' and 'else', find the next conditional directive or endif
      for (let i = line.number + 1; i <= doc.lines; i++) {
        const nextLine = doc.line(i);
        const nextText = nextLine.text.trim();

        // Look for next conditional compilation directive or endif
        if (/^:-\s+(elif|else|endif)(?:\(|\.)/.test(nextText)) {
          // For conditional compilation, fold even single lines (logical blocks)
          if (i > line.number) {
            return { from: lineEnd, to: nextLine.from };
          }
          return null;
        }
      }

      // If no closing directive found, fold to end of document
      if (doc.lines > line.number) {
        return { from: lineEnd, to: doc.length };
      }
    }
  }

  return null;
}

// Export complete language support bundle
export const logtalkSupport = [
  logtalk,
  indentUnit.of('\t'),
  indentOnInput(),
  foldService.of(logtalkFoldService),
  Prec.high(keymap.of([
    { key: "Enter", run: logtalkEnterHandler },
    indentWithTab
  ]))
]
