/**
 * Copyright Aquera Inc 2023
 *
 * This source code is licensed under the BSD-3-Clause license found in the
 * LICENSE file in the root directory of this source tree.
 */

import {
  html,
  CSSResultArray,
  TemplateResult,
  PropertyValueMap,
} from 'lit';

import { customElement, query, property } from 'lit/decorators.js';
import { styles } from './nile-code-editor.css';
import { EditorView } from 'codemirror';
import { ViewUpdate, placeholder, tooltips } from '@codemirror/view';
import { 
  Compartment,
  EditorState,
  Extension
} from '@codemirror/state';
import { StyleSpec } from 'style-mod';
import {
  syntaxHighlighting,
  defaultHighlightStyle,
} from '@codemirror/language';

import { lineNumbers } from '@codemirror/view';
import {
  javascript,
  javascriptLanguage,
} from '@codemirror/lang-javascript';
import { sql } from '@codemirror/lang-sql';
import { json } from '@codemirror/lang-json';
import { html as htmlLang } from '@codemirror/lang-html';
import { autocompletion,acceptCompletion, closeCompletion,CompletionContext,CompletionResult, completionStatus } from '@codemirror/autocomplete';
import NileElement from '../internal/nile-element';
import { basicSetup } from './extensionSetup';
import { classMap } from 'lit/directives/class-map.js';
import { Theme as DefaultTheme, customisedThemeCss, fontFamily, readOnlyTheme } from './theme';
import { keymap } from '@codemirror/view';

// Choose the appropriate mode for your use case

/**
 * Nile icon component.
 *
 * @tag nile-code-editor
 *
 */
@customElement('nile-code-editor')
export class NileCodeEditor extends NileElement {
  
  @query('.code__editor__container') codeEditor: HTMLInputElement;

  @property({ type: String, reflect: true , attribute: true }) value = '';

  @property({ type: String, reflect: true , attribute: true }) expandIcon = "var(--nile-icon-expand-2, var(--ng-icon-expand-06))";

  @property({ type: String, reflect: true , attribute: true }) placeholder = "";

  @property({ type: Object, reflect: true , attribute: true }) customAutoCompletions: object | any = {};

  @property({ type: Array, reflect: true , attribute: true }) customCompletionsPaths: string[] = [];

  @property({ type: String, reflect: true , attribute: true}) language: 'javascript' | 'sql' | 'json' | 'html' = 'javascript';

  @property({ type: String, reflect: true , attribute: 'error-message' }) errorMessage: string = '';

  @property({ type: Boolean, reflect: true , attribute: true }) error: boolean = false;

  @property({ type: Boolean, reflect: true , attribute: true }) enableSearch: boolean = false;

  @property({ type: Boolean, reflect: true , attribute: true }) noborder: boolean = false;

  @property({ type: Boolean, reflect: true , attribute: true }) multiline: boolean = false;

  @property({ type: Boolean, reflect: true , attribute: true }) enableFoldGutters: boolean = false;
  
  @property({ type: Boolean, reflect: true , attribute: true }) allowVariableInCustomSuggestion: boolean = false;

  @property({ type: Boolean, reflect: true , attribute: true }) lineNumbers: boolean = false;

  @property({ type: Boolean, reflect: true , attribute: false }) disableSyntaxHighlighting: boolean = false;

  @property({ type: Object,  attribute: false }) customThemeCSS: object | null = customisedThemeCss;

  @property({ type: Boolean, reflect: true , attribute: true }) lineNumbersMultiline: boolean = true;

  @property({ type: Boolean, reflect: true , attribute: true }) hasScroller: boolean = true;

  @property({ type: Boolean, reflect: true , attribute: true }) expandable: boolean = true;

  @property({ type: Boolean, reflect: true , attribute: true}) readonly: boolean = false;

  @property({ type: Boolean, reflect: true , attribute: true}) disabled: boolean = false;

  @property({ type: Boolean, reflect: true , attribute: true}) debounce: boolean = false;

  @property({ type: Number, reflect: true , attribute: true}) debounceTimeout: number = 200;

  @property({ type: Boolean, reflect: true , attribute: true}) aboveCursor: boolean = false;

  @property({ type: Boolean, reflect: true, attribute: true }) tabCompletion: boolean = true;

  @property({ type: Boolean, reflect: true, attribute: true }) defaultFont: boolean = false;

  @property({ type: Object, attribute: false }) autoCompleteStyle: { width?: string; multiline?: boolean } | undefined = undefined;

  @property({ type: Boolean, reflect: true, attribute: true }) hideReadOnlyCursor: boolean = false; 

  public view: EditorView;
  public viewState:EditorState;
  private timeOut: any = null;
  private resizeObserver?: ResizeObserver;

  // Compartments for initialiazing and switching extensions
  private lineNumbersComp = new Compartment();
  private restrictSingleLineComp = new Compartment();
  private readOnlyComp = new Compartment();
  private customCompletionComp = new Compartment();
  private placeholderComp = new Compartment();
  private defaultSyntaxHighlightingComp = new Compartment();
  private themeComp = new Compartment();
  private autoCompletionComp = new Compartment();
  private autoCompleteStyleComp = new Compartment();

  private isSpacePressed:boolean = false;
  /**
   * The styles for CodeEditor
   * @remarks If you are extending this class you can extend the base styles with super. Eg `return [super(), myCustomStyles]`
   */
  public static get styles(): CSSResultArray {
    return [styles];
  }

  connectedCallback(): void {
    super.connectedCallback();
    this.emit('nile-init',undefined,false);
  }

  disconnectedCallback(): void {
    super.disconnectedCallback();
    this.resizeObserver?.disconnect();
    this.view.destroy()
    this.emit('nile-destroy',undefined,false);
  }

  firstUpdated() {
    this.createNewView()
    
    if (this.autoCompleteStyle?.width) {
      this.resizeObserver = new ResizeObserver(entries => {
        for (const entry of entries) {
          const width = entry.contentRect.width;
          this.updateAutocompleteWidth(width);
        }
      });
      this.resizeObserver.observe(this.codeEditor);
    }
    
    this.emit('nile-after-init',{ 
      codeMirrorInstance: this.view, 
      createNewView: this.createNewView, 
      insertAtCursor: this.insertBetweenCode 
    }, false )
  }

  getTabCompletionKeymap() {
    return this.tabCompletion
      ? keymap.of([
          {
            key: 'Tab',
            run: acceptCompletion,
          }
        ])
      : [];
  }

  protected updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void{
    super.updated(changedProperties);
    if (changedProperties.has('value') && this.view.state.doc.toString()!=this.value) {
      // Editor has already been initialized, update its state
      this.singleLineMultiLineToggle();
    }
    if (changedProperties.has('multiline')) {
      this.view.dispatch({
        effects: [
          this.lineNumbersComp.reconfigure(this.getLineNumbersExension()),
          this.restrictSingleLineComp.reconfigure(this.getSingleLineExtension())
        ],
      })
      this.singleLineMultiLineToggle();
    }
    if (changedProperties.has('readonly')) {
      this.view.dispatch({ 
        effects: [
          this.readOnlyComp.reconfigure(this.getReadOnlyExtension()),
        ]
      })
    }
    if (changedProperties.has('disabled') && this.view) {
      this.view.dispatch({ 
        effects: [
          this.readOnlyComp.reconfigure(this.getReadOnlyExtension()),
        ]
      })
    }
    if (changedProperties.has('aboveCursor')) {
      this.view.dispatch({
        effects: [
          this.autoCompletionComp.reconfigure(
            autocompletion({ aboveCursor: this.aboveCursor })
          )
        ]
      });
    }    
    if (changedProperties.has('placeholder')) {
      this.view.dispatch({ 
        effects: [
          this.placeholderComp.reconfigure(this.getPlaceholderExtension()),
        ]
      })
    }
    if (changedProperties.has('lineNumbers') || changedProperties.has('lineNumbersMultiline')) {
      this.view.dispatch({ 
        effects: [
          this.lineNumbersComp.reconfigure(this.getLineNumbersExension()),
        ]
      })
    }
    if(changedProperties.has('customAutoCompletions') || changedProperties.has('customCompletionsPaths')){
      this.view.dispatch({ 
        effects: [
          this.customCompletionComp.reconfigure(javascriptLanguage.data.of({
            autocomplete: this.customAutocomplete
          }))
        ]
      })
    }
    if(changedProperties.has('disableSyntaxHighlighting')){
      this.view.dispatch({ 
        effects: [
          this.defaultSyntaxHighlightingComp.reconfigure(this.getDefaultSyntaxHighlightingExtension())
        ]
      })
    }
    if(changedProperties.has('customThemeCSS')){
      this.view.dispatch({ 
        effects: [
          this.themeComp.reconfigure(this.getCustomThemeExtension())
        ]
      })
    }
    if(changedProperties.has('autoCompleteStyle') && this.view){
      if (this.autoCompleteStyle?.width) {
        if (!this.resizeObserver) {
          this.resizeObserver = new ResizeObserver(entries => {
            for (const entry of entries) {
              const width = entry.contentRect.width;
              this.updateAutocompleteWidth(width);
            }
          });
          this.resizeObserver.observe(this.codeEditor);
        }
      } else {
        this.resizeObserver?.disconnect();
        this.resizeObserver = undefined;
      }
      
      this.view.dispatch({ 
        effects: [
          this.autoCompleteStyleComp.reconfigure(this.getAutoCompleteStyleExtension())
        ]
      })
    }
  }

  public render(): TemplateResult {
    const hasErrorMessage = !!this.errorMessage;
    const hasError = !!this.error;
    const noborder = !!this.noborder;
    const noScrollbar = !this.hasScroller
    return html`<div
        part="code-editor-base"
        class=${classMap({
          'code__editor__container': true,
          'noborder': noborder,
          'error': hasError || hasErrorMessage,
          'code__editor__singleline': !this.multiline,
          'no-scroller': noScrollbar,
          'disabled': this.disabled
        })}
        tabindex=${this.disabled ? '-1' : '0'}
      >
      ${this.expandable && !this.disabled
      ? html` 
        <div @click="${(e: CustomEvent) => this.emit('nile-expand')}" class="expand__icon__container">
          <nile-icon
            name="${this.expandIcon}"
            method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))"
            size="16"
            color="black"
          ></nile-icon>
        </div>`
      : ''}
      </div>
      ${hasErrorMessage
        ? html`
            <nile-form-error-message
              >${this.errorMessage}</nile-form-error-message
            >
          `
        : ``}`;
  }

  createNewView(emitEvent=true){
    if(this.view) this.view.destroy();
    this.createState()
    this.view = new EditorView({
      state: this.viewState,
      parent: this.codeEditor
    });

    this.view.dom.addEventListener('keydown', (e: KeyboardEvent) => {
      this.checkForSpaceKey(e);
      this.handleEscapeKey(e);
   }, true);
    if (emitEvent) this.emit('nile-after-update', { createNewView: this.createNewView, codeMirrorInstance: this.view,  }, false );
  }

  checkForSpaceKey(e: KeyboardEvent) {
    if (e.code === 'Space' && !e.ctrlKey) {
        this.isSpacePressed = true;
      } else {
        this.isSpacePressed = false;
    }
  }

   public closeCompletion() {
    if (this.view) {
      closeCompletion(this.view);
    }
  }

  handleEscapeKey(e: KeyboardEvent) {
    if (e.key === 'Escape' && this.view && completionStatus(this.view.state) !== null) {
      e.stopPropagation();      
      e.preventDefault();         
      closeCompletion(this.view);
    }
  }

  createState(){
    const lineNumbersExtension = this.lineNumbersComp.of(this.getLineNumbersExension());
    const readOnlyExtension = this.readOnlyComp.of(this.getReadOnlyExtension());
    const restrictSingleLineExtension = this.restrictSingleLineComp.of(this.getSingleLineExtension())
    const placeholderExtension = this.placeholderComp.of(this.getPlaceholderExtension())
    const customThemeExtension=this.themeComp.of(this.getCustomThemeExtension());
    const defaultSyntaxHighlightingExtension = this.defaultSyntaxHighlightingComp.of(this.getDefaultSyntaxHighlightingExtension());
    const autoCompleteStyleExtension = this.autoCompleteStyleComp.of(this.getAutoCompleteStyleExtension());
    const language = this.getLanguageExtension()
    const customAutoCompletions = this.customCompletionComp.of(javascriptLanguage.data.of({
      autocomplete: this.customAutocomplete
    }));

    this.viewState = EditorState.create({
      doc: !this.multiline ? convertToSingleLine(this.value) : this.value,
      extensions: [
        basicSetup({
          highlightActiveLine: false,
          foldGutter: this.enableFoldGutters,
          enableSearch:this.enableSearch
        }),
        lineNumbersExtension,
        readOnlyExtension,
        restrictSingleLineExtension,
        customAutoCompletions,
        placeholderExtension,
        defaultSyntaxHighlightingExtension,
        this.autoCompletionComp.of(
          autocompletion({ aboveCursor: this.aboveCursor })
        ),
        language,
        customThemeExtension,
        autoCompleteStyleExtension,
        this.getTabCompletionKeymap(),
        EditorView.updateListener.of((v: ViewUpdate) => {
          if (v.docChanged) {
            this.debounce ? this.emitAfterTimeout({ value: this.view.state.doc.toString() }) : this.emit('nile-change', { value: this.view.state.doc.toString() })
          }
        }),
        EditorView.domEventHandlers({
          focus: () => this.dispatchEvent(new Event('nile-focus')),
          blur: () => this.dispatchEvent(new Event('nile-blur')),
        }),
      ],
    });
    return this.viewState
  }
  /**
   * Custom autocomplete handler for code editor suggestions
   * @param context CompletionContext from CodeMirror
   * @returns CompletionResult with suggestions or null if no suggestions
   */
  customAutocomplete = (context: CompletionContext): CompletionResult | null => {
    // Getting the valid last line, last text from the code editor
    const text = context.state.doc.sliceString(0, context.pos);
    const lastWord = text.split('\n').at(-1)?.split(' ').at(-1) || '';
    const [textBeforeCursor, baseTextAfterSeperation] = splitStringAtLastSeparator(lastWord);
    
    return this.getNestedSuggestions(context, textBeforeCursor, baseTextAfterSeperation) 
    || this.getTopLevelSuggestions(context, textBeforeCursor);
  };

  /**
   * Gets nested property suggestions based on the current path
   * @param context CompletionContext from CodeMirror
   * @param textBeforeCursor Text before cursor position
   * @param baseTextAfterSeperation Text after the last separator (. or [)
   * @returns CompletionResult with nested suggestions or null
   */
  getNestedSuggestions(context: CompletionContext, textBeforeCursor: string, baseTextAfterSeperation: string) {
    // Return early if not a valid path or not ending with . or [
    if (!isValidPath(textBeforeCursor) || !['.', '['].includes(textBeforeCursor.at(-1)!)) {
      return null;
    }

    const path = parsePath(textBeforeCursor);
    if (!path) return null;

    const textAfterSeperation = baseTextAfterSeperation.replace(/["'\[]/g, '');
    const isInString = textAfterSeperation !== baseTextAfterSeperation;
    const isBracket = textBeforeCursor.at(-1) === '[';

    // Return null if we're in a string after a dot
    if (textBeforeCursor.at(-1) === '.' && isInString) return null;

    // Get nested properties and filter by text after separation if it exists
    let resolved = resolveNestedProperties(this.customAutoCompletions, path);
    if (!resolved || typeof resolved !== 'object') return null;

    if (textAfterSeperation) {
      resolved = Object.fromEntries(
        Object.entries(resolved).filter(([key]) => 
          key.toLowerCase().startsWith(textAfterSeperation.toLowerCase())
        )
      );
    }

    return {
      from: context.pos - textAfterSeperation.length,
      options: Object.keys(resolved).map(key => ({
        label: key,
        type: 'property',
        info: `Key of ${path[path.length - 1]}`,
        apply: !this.allowVariableInCustomSuggestion && (isBracket && !isInString) 
          ? `'${key}'` 
          : key,
        boost: 999
      }))
    };
  }

  /**
   * Gets top level suggestions based on custom completions and paths
   * @param context CompletionContext from CodeMirror
   * @param textBeforeCursor Text before cursor position
   * @returns CompletionResult with top level suggestions or null
   */
  getTopLevelSuggestions(context: CompletionContext,textBeforeCursor:string){
    const baseMatch: any = textBeforeCursor.match(/([a-zA-Z_$][\w$]*)$/);
    
    if (!baseMatch) {
      const trimmedText = textBeforeCursor.trim();
      if (trimmedText === '' && !this.isSpacePressed) {
        const optionsList = Object.keys(this.customAutoCompletions).filter(key => 
          Object.keys(this.customAutoCompletions[key]).length
        );
        
        const options = optionsList.map((key) => ({
          label: key,
          type: 'property',
          apply: key,
          boost: 999
        }));
        
        if(this.customCompletionsPaths.length){
          this.customCompletionsPaths.forEach(path => {
            options.push({
              label: ''+path,
              type: 'property',
              apply: ''+path,
              boost: 998
            });
          });
        }
        
        return {
          from: context.pos,
          options: options
        };
      }
      return null;
    }
  
    const optionsList = Object.keys(this.customAutoCompletions).filter(key => 
      Object.keys(this.customAutoCompletions[key]).length && 
      key.toLowerCase().startsWith(textBeforeCursor.toLowerCase())
    );
    
    const options=optionsList.map((key) => ({
      label: key,
      type: 'property',
      apply: key,
      boost: 999
    }))
    if(this.customCompletionsPaths.length){
      this.customCompletionsPaths
      .filter(path=>path.toLocaleLowerCase().includes(textBeforeCursor.toLocaleLowerCase()))
      .forEach(path=>{
        options.push({
          label: ''+path,
          type: 'property',
          apply: ''+path,
          boost: 998
        })
      })
    }
    return {
      from: context.pos - baseMatch[1].length,
      options: options
    }
  }

  emitAfterTimeout(value:any){
    if(this.timeOut) clearTimeout(this.timeOut);
    this.timeOut=setTimeout(()=> this.emit('nile-change', value, false), this.debounceTimeout)
  }
  
  public focusAtPosition(pos: number=this.view.state.doc.toString().length): void {
    if (this.view) {
      this.view.dispatch({
        selection: { anchor: pos },
      });
      this.view.focus();
    }
  }

  public insertBetweenCode=(text: string) => {
    const transaction = this.view.state.changeByRange(range => {
      const { from, to } = range;
      return { changes: { from:from, to, insert: text }, range };
    });
    this.view.dispatch(transaction);
  }

  singleLineMultiLineToggle() {
    this.view.dispatch({
      changes: {
        from: 0,
        to: this.view.state.doc.length,
        insert: !this.multiline
          ? convertToSingleLine(this.value)
          : this.value,
      },
    });
  }

  //EXTENSION CONFIGURATIONS
  getLineNumbersExension() {
    return  (!this.multiline && this.lineNumbers) || (this.multiline && this.lineNumbersMultiline) ? lineNumbers() : [];
  }
  
  getLanguageExtension():Extension{
    switch(this.language){
      case 'sql':
        return sql();
      case 'json':
        return json();
      case 'html':
        return htmlLang();
      default:
        return javascript();   
    }
  }

  getReadOnlyExtension() {
    if (this.disabled) {
      return [
        EditorState.readOnly.of(true),
        EditorView.editable.of(false),
      ];
    }

    if (this.readonly) {
      const extensions = [EditorState.readOnly.of(true)];
      if (this.hideReadOnlyCursor) {
        extensions.push(EditorView.theme(readOnlyTheme));
      }
      return extensions;
    }

    return [];
  }

  getSingleLineExtension() {
    return !this.multiline ? EditorState.transactionFilter.of(tr =>
      tr.newDoc.lines > 1 ? [] : tr
    ) : [];
  }

  getPlaceholderExtension(){
    return this.placeholder ? placeholder(this.placeholder) : [];
  }
  
  getDefaultSyntaxHighlightingExtension(){
    return !this.disableSyntaxHighlighting ? syntaxHighlighting(defaultHighlightStyle, { fallback: true }):[]
  }

  getCustomThemeExtension(): Extension {
    if(this.customThemeCSS) {
      if(this.defaultFont){
        return [EditorView.theme(this.customThemeCSS as { [selector: string]: StyleSpec }), EditorView.theme(customisedThemeCss),EditorView.theme(fontFamily)];
      }
      return [EditorView.theme(this.customThemeCSS as { [selector: string]: StyleSpec }), EditorView.theme(customisedThemeCss)];
    }
    if(this.defaultFont){
      return [EditorView.theme(customisedThemeCss), EditorView.theme(fontFamily)];
    }
    return [EditorView.theme(customisedThemeCss)];
  }

  getAutoCompleteStyleExtension(width?: number): Extension {
      if (this.autoCompleteStyle?.width) {
        const containerWidth = width || (this.codeEditor ? this.codeEditor.getBoundingClientRect().width : 300);
        const widthPercentage = parseFloat(this.autoCompleteStyle.width.replace('%', ''));
        const codeEditorWidth = (containerWidth * widthPercentage) / 100;
        
        const multilineStyles: { [key: string]: string } = this.autoCompleteStyle.multiline ? {
          overflow: "visible",
          textOverflow: "unset",
          whiteSpace: "normal",
          wordWrap: "break-word"
        } : {
          overflow: "hidden",
          textOverflow: "ellipsis",
          whiteSpace: "nowrap"
        };
        
        return [ 
          tooltips({
            position: "fixed", 
            tooltipSpace: () => {
              const rect = this.codeEditor.getBoundingClientRect();
              return {
                left: rect.left,
                top: 0,
                right: rect.right,
                bottom: 1000
              };
            }
          }),
          EditorView.theme({
          ".cm-tooltip.cm-tooltip-autocomplete": {
            maxWidth: (codeEditorWidth - 15) + "px !important"
          },
          ".cm-tooltip.cm-tooltip-autocomplete > ul > li": {
            ...multilineStyles,
            maxWidth: "100%"
          },
          ".cm-tooltip.cm-tooltip-autocomplete > ul": {
            minWidth: "unset !important"
          }
        })
      ];
    }
    return [];
  }

  private updateAutocompleteWidth(width: number) {
    if (this.view) {
      this.view.dispatch({
        effects: this.autoCompleteStyleComp.reconfigure(this.getAutoCompleteStyleExtension(width))
      });
    }
  }

  restrictSingleLine() {
    return EditorState.transactionFilter.of(tr =>
      tr.newDoc.lines > 1 ? [] : tr
    );
  }
  /* #endregion */
}

export default NileCodeEditor;

declare global {
  interface HTMLElementTagNameMap {
    'nile-code-editor': NileCodeEditor;
  }
}

/**
 * Parses a string path into an array of keys representing nested object access
 * @param text The path string to parse (e.g. "foo.bar[0].baz")
 * @returns Array of keys if valid path, null otherwise
 * @example
 * parsePath("foo.bar[0]") // returns ["foo", "bar", "0"]
 * parsePath("invalid") // returns null
 */
function parsePath(text: string) {
  const regex = /([a-zA-Z_$][\w$]*)(\[(?:[^\]]+)\]|\.[a-zA-Z_$][\w$]*)*/g;
  const matches = [...text.matchAll(regex)];
  if (matches.length > 0) {
    const base = matches[0][1]; // The base object name
    const keys = [base];
    // Extract keys from dot or bracket notation
    const pathMatches = text.match(/\[(.*?)\]|\.(\w+)/g) || [];
    for (const match of pathMatches) {
      if (match.startsWith('[')) {
        keys.push(match.slice(1, -1).replace(/['"]/g, '')); // Remove brackets and quotes
      } else if (match.startsWith('.')) {
        keys.push(match.slice(1));
      }
    }
    return keys;
  }
  return null;
};

/**
 * Splits a path string at the last separator (. or [)
 * @param input The path string to split
 * @returns Array containing [path up to last separator, remainder after separator]
 * @example
 * splitStringAtLastSeparator("foo.bar[0]") // returns ["foo.bar[", "0"]
 */
function splitStringAtLastSeparator(input:string) {
  const lastSeparatorIndex = Math.max(input.lastIndexOf('.'), input.lastIndexOf('['));
  if (lastSeparatorIndex === -1) return [input, ''];
  return [input.slice(0, lastSeparatorIndex + 1), input.slice(lastSeparatorIndex + 1)];
}

/**
 * Traverses an object using an array of keys to access nested properties
 * @param obj The object to traverse
 * @param keys Array of keys defining the path to the desired property
 * @returns The value at the specified path, or null if path is invalid
 * @example
 * resolveNestedProperties({foo: {bar: 123}}, ["foo", "bar"]) // returns 123
 */
function resolveNestedProperties (obj:any, keys:any[]){
  return keys.reduce((acc, key) => {
    if (acc && typeof acc === 'object') {
      return acc[key];
    }
    return null;
  }, obj);
};

/**
 * Validates if a string represents a valid object path format
 * @param path The path string to validate
 * @returns Boolean indicating if path format is valid
 * @example
 * isValidPath("foo.bar[0]") // returns true
 * isValidPath("foo..bar") // returns false
 */
function isValidPath(path: string) {
  // Regex to validate the format of the string
  const regex = /^([a-zA-Z_$][\w$]*)(\.[a-zA-Z_$][\w$]*|\[\s*(['"]?[a-zA-Z0-9_$]*['"]?)\s*\])*([\.\[])?$/;
  // Test the string against the regex
  return regex.test(path);
}

/**
 * Converts multi-line code into a single line by removing line breaks and extra whitespace
 * @param code The code string to convert
 * @returns Single line version of the code
 * @example
 * convertToSingleLine("foo\n  bar") // returns "foo bar"
 */
function convertToSingleLine(code: string) {
  if (!code) return '';
  // Remove line breaks and unnecessary whitespace
  return code.replace(/\s+/g, ' ').trim();
}

