import type { SyntaxNode, Tree } from "@lezer/common";
import type { Format, MarkdownViewMode, TokenLevel, Delimiter, TokenStatus, Field, DisplayBehaviour } from "src/enums";
import { Line, RangeSet, RangeValue, Text, Range } from "@codemirror/state"
import { Decoration, DecorationSet } from "@codemirror/view";
import { ColorComponent, DropdownComponent, ExtraButtonComponent, IconName, SliderComponent, TextAreaComponent, TextComponent, ToggleComponent } from "obsidian";
import ExtendedMarkdownSyntax from "main";

/** Token interface */
export type Token = {
    /** Format type */
    type: Format, // 3-bit
    /** Either block or inline */
    level: TokenLevel, // 1 bit
    /** Token status */
    status: TokenStatus, // 2-bit
    /** Start offset */
    from: number, // 16-bit
    /** End offset */
    to: number, // 16-bit
    /** Length of its opening delimiter. */
    openLen: number,
    /** Length of its closing delimiter if any, otherwise it should be zero. */
    closeLen: number,
    /** Length of its tag if any, otherwise it should be zero. */
    tagLen: number,
    /** Whether the range of tag is covered by the range of content or not. */
    tagAsContent: boolean;
    /** The tag was considered as invalid when it does not satisfy its rule. */
    validTag: boolean;
    /**
     * Closed by blank line instead of closing delimiter. If `closeLen` is 0, the
     * token's type isn't space-restricted format, and `closedByBlankLine` is `false`,
     * then indicates that the token definitely was located in the end of the document
     * (`Token.to == Doc.length`).
     */
    closedByBlankLine: boolean;
}

/** Accumulation of `ChangeSet` generated by the `composeChange` function. */
export type ChangedRange = {
    /**
     * Start offset of a changed range, measured from
     * the text both before and after the change.
     */
    from: number,
    /** End offset of a changed range, measured from the doc before the change. */
    initTo: number,
    /** End offset of a changed range, measured from the doc after the change. */
    changedTo: number;
    /**
     * Length difference. Can be negative value, indicating that replaced text
     * is longer than the substitute.
     */
    length: number;
}

/** Describe the rule that has to be satisfied by the delimiter */
export type DelimSpec = {
    /** Should be single character */
    char: string,
    /** Delimiter length */
    length: number,
    /**
     * If true, then delimiter length must be the same as
     * in the predetermined rule. Otherwise, the defined length
     * act as minimum length.
     */
    exactLen: boolean,
    /** Should be `Delimiter.OPEN` or `Delimiter.CLOSE` */
    role: Delimiter,
    /**
     * When true, any space after the opening delimiter or before the closing
     * one doesn't make the delimiter invalid. Default is false.
     */
    allowSpaceOnDelim?: boolean
}

/**
 * Used for (re)configuring the state, especially in
 * the case of document or tree change
 */
export type StateConfig = {
    doc: Text,
    tree: Tree,
    offset: number,
    settings: PluginSettings,
    maxLine?: number
}

/**
 * Region defined here as a `PlainRange` collection arranged in an array.
 * Each range inside should be sorted in order according to its `from`.
 */
export type Region = PlainRange[];

export type NodeSpec = {
    node: SyntaxNode,
    type: string
}

export type InlineFormat = Extract<Format, Format.INSERTION | Format.SPOILER | Format.SUPERSCRIPT | Format.SUBSCRIPT | Format.HIGHLIGHT | Format.CUSTOM_SPAN>;

export type BlockFormat = Exclude<Format, InlineFormat>;

export type NonBuiltinInlineFormat = Exclude<InlineFormat, Format.HIGHLIGHT>;

export type TokenGroup = Token[];

export type ColorConfig = {
    tag: string,
    name: string,
    color: string,
    showInMenu: boolean
};

export type PluginSettings = {
    insertion: MarkdownViewMode;
    spoiler: MarkdownViewMode;
    superscript: MarkdownViewMode;
    subscript: MarkdownViewMode;
    customHighlight: MarkdownViewMode;
    customSpan: MarkdownViewMode;
    fencedDiv: MarkdownViewMode;
    editorEscape: boolean;
    colorButton: boolean;
    decoratePDF: boolean;
    colorConfigs: ColorConfig[];
    lightModeHlOpacity: number;
    darkModeHlOpacity: number;
    hlTagDisplayBehaviour: DisplayBehaviour;
    spanTagDisplayBehaviour: DisplayBehaviour;
    showHlTagInPreviewMode: boolean;
    showSpanTagInPreviewMode: boolean;
    alwaysShowFencedDivTag: MarkdownViewMode;
}

export type PlainRange = { from: number, to: number };

export type InlineFormatRule = {
    char: string,
    length: number,
    exactLen: boolean,
    allowSpace: boolean,
    mustBeClosed: boolean,
    class: string,
    getEl: (cls?: string) => Element,
    builtin: boolean
}

export type BlockFormatRule = {
    char: string,
    length: number,
    exactLen: boolean,
    class: string,
}

export type FormatRule = InlineFormatRule & BlockFormatRule;

export interface TokenDecoration extends Decoration {
    spec: {
        class: string,
        token: Token
    };
}

export type TokenDecorationSet = RangeSet<TokenDecoration>;

export type IndexCache = { number: number }

export type DecoSetCollection = Record<"inlineSet" | "blockSet" | "omittedSet" | "colorBtnSet" | "revealedSpoilerSet", DecorationSet>;

export type IterLineSpec = {
    doc: Text,
    fromLn: number,
    toLn?: number,
    callback: (line: Line, doc: Text) => boolean | void
}

export type IterTokenGroupSpec = {
    tokens: TokenGroup,
    ranges: PlainRange[] | readonly PlainRange[],
    indexCache: IndexCache
    callback: (token: Token) => unknown,
}

export type RangeSetUpdate<T extends RangeValue> = {
    add?: readonly Range<T>[];
    sort?: boolean;
    filter?: (from: number, to: number, value: T) => boolean;
    filterFrom?: number;
    filterTo?: number;
}

export type OptionType = number;

export type Options<T, U = string> = T extends OptionType ? Record<OptionType, U> : never;

export type FieldComponentRecord = {
    [Field.TOGGLE]: ToggleComponent,
    [Field.MULTI_TOGGLE]: ExtraButtonComponent,
    [Field.DROPDOWN]: DropdownComponent,
    [Field.COLOR]: ColorComponent,
    [Field.TEXT]: TextComponent,
    [Field.TEXT_AREA]: TextAreaComponent,
    [Field.SLIDER]: SliderComponent
};

export type FieldValueRecord = {
    [Field.TOGGLE]: boolean,
    [Field.MULTI_TOGGLE]: OptionType,
    [Field.DROPDOWN]: OptionType,
    [Field.COLOR]: string,
    [Field.TEXT]: string,
    [Field.TEXT_AREA]: string,
    [Field.SLIDER]: number
};

export type FieldSpecRecord<TValue> = {
    [Field.TOGGLE]: null,
    [Field.MULTI_TOGGLE]: { options: Options<TValue, { icon: IconName, tooltip?: string }> },
    [Field.DROPDOWN]: { options: Options<TValue> },
    [Field.COLOR]: null,
    [Field.TEXT]: { placeholder?: string },
    [Field.TEXT_AREA]: { placeholder?: string, resizeable?: boolean }
    [Field.SLIDER]: { min: number, max: number, step: number }
};

export type FieldValue<TField extends Field> = FieldValueRecord[TField];

export type FieldComponent<TField extends Field> = FieldComponentRecord[TField];

export type FieldSpec<TField extends Field, TValue> = FieldSpecRecord<TValue>[TField];

export type AbstractFieldDesc<TField extends Field, TRecord, TKey extends keyof TRecord> = {
    type: TField,
    record: TRecord extends Record<string, unknown> ? TRecord : never,
    key: TKey extends keyof TRecord ? TRecord[TKey] extends infer TValue ? TValue extends FieldValue<TField> ? TKey : never : never : never,
    callback?: (component: FieldComponent<TField>, plugin: ExtendedMarkdownSyntax) => unknown,
    spec: FieldSpec<TField, TRecord[TKey]>,
    update?: {
        stylesheet?: boolean,
        internal?: boolean
    }
};

export type ToggleFieldDesc<TRecord> = AbstractFieldDesc<Field.TOGGLE, TRecord, keyof TRecord>;
export type MultiToggleFieldDesc<TRecord> = AbstractFieldDesc<Field.MULTI_TOGGLE, TRecord, keyof TRecord>;
export type DropdownFieldDesc<TRecord> = AbstractFieldDesc<Field.DROPDOWN, TRecord, keyof TRecord>;
export type ColorFieldDesc<TRecord> = AbstractFieldDesc<Field.COLOR, TRecord, keyof TRecord>;
export type TextFieldDesc<TRecord> = AbstractFieldDesc<Field.TEXT, TRecord, keyof TRecord>;
export type TextAreaFieldDesc<TRecord> = AbstractFieldDesc<Field.TEXT_AREA, TRecord, keyof TRecord>;
export type SliderFieldDesc<TRecord> = AbstractFieldDesc<Field.SLIDER, TRecord, keyof TRecord>;

export type FieldGroup<TRecord> = (
    ToggleFieldDesc<TRecord> |
    MultiToggleFieldDesc<TRecord> |
    DropdownFieldDesc<TRecord> |
    ColorFieldDesc<TRecord> |
    TextFieldDesc<TRecord> |
    TextAreaFieldDesc<TRecord> |
    SliderFieldDesc<TRecord>
)[];

export type SettingItem<TRecord> = {
    name: string,
    desc?: string,
    fields?: FieldGroup<TRecord>,
    preservedForColorSettings?: boolean
};

export type SettingGroup<TRecord> = {
    heading?: string,
    desc?: string,
    collapsible?: boolean,
    items: SettingItem<TRecord>[]
};

export type SettingRoot<TRecord> = SettingGroup<TRecord>[];