
import * as cdecode from 'cdecode/client';
import * as vscode from 'cdecode/client';
import { mixin } from '@vscode/monaco-editor/esm/vs/base/common/objects';
import * as typeConvert from '@workbench-stack/extension-api/lib/core/converters/ext-host-type-converters';
import { Range, Position, Disposable, CompletionList, SnippetString, CodeActionKind, DocumentSymbol } from '@workbench-stack/extension-api/lib/core/types/ext-host-types';
import * as modes from '@vscode/monaco-editor/esm/vs/editor/common/modes';
import { URI, UriComponents } from '@vscode/monaco-editor/esm/vs/base/common/uri';
import { ISerializedDocumentFilter, CodeActionDto, SuggestResultDto, SuggestionDto, LinkDto, IRawColorInfo, IdObject } from '@workbench-stack/extension-api/lib/protocol/common.protocol';
import { Range as EditorRange } from '@vscode/monaco-editor/esm/vs/editor/common/core/range';
import { isFalsyOrEmpty, isNonEmptyArray, range } from '@vscode/monaco-editor/esm/vs/base/common/arrays';
import { isObject } from '@vscode/monaco-editor/esm/vs/base/common/types';
import { ObjectIdentifier, WorkspaceSymbolsDto, WorkspaceSymbolDto } from '@workbench-stack/extension-api';
import { generateUuid } from '@vscode/monaco-editor/esm/vs/base/common/uuid';
import { logger } from '@cdm-logger/client';

export type CodeInsetDto = ObjectIdentifier;

export interface CustomCodeAction extends CodeActionDto {
    _isSynthetic?: boolean;
}

export class ExtHostHeapService {

    private static _idPool = 0;

    private _data = new Map<number, any>();

    public keep(obj: any): number {
        const id = ExtHostHeapService._idPool++;
        this._data.set(id, obj);
        return id;
    }

    public delete(id: number): boolean {
        return this._data.delete(id);
    }

    public get<T>(id: number): T {
        return this._data.get(id);
    }

    public $onGarbageCollection(ids: number[]): void {
        for (const id of ids) {
            this.delete(id);
        }
    }
}

export interface ISchemeTransformer {
    transformOutgoing(scheme: string): string;
}

export function _asDocumentSymbolTree(info: cdecode.SymbolInformation[]): modes.DocumentSymbol[] {
    // first sort by start (and end) and then loop over all elements
    // and build a tree based on containment.
    info = info.slice(0).sort((a, b) => {
        // tslint:disable-next-line:no-shadowed-variable
        let res = a.location.range.start.compareTo(b.location.range.start);
        if (res === 0) {
            res = b.location.range.end.compareTo(a.location.range.end);
        }
        return res;
    });
    let res: modes.DocumentSymbol[] = [];
    let parentStack: modes.DocumentSymbol[] = [];
    for (let i = 0; i < info.length; i++) {
        let element = <modes.DocumentSymbol>{
            name: info[i].name || '!!MISSING: name!!',
            kind: typeConvert.SymbolKind.from(info[i].kind),
            detail: undefined!, // Strict null override — avoid changing behavior,
            containerName: info[i].containerName,
            range: typeConvert.Range.from(info[i].location.range),
            selectionRange: typeConvert.Range.from(info[i].location.range),
            children: [],
        };

        while (true) {
            if (parentStack.length === 0) {
                parentStack.push(element);
                res.push(element);
                break;
            }
            let parent = parentStack[parentStack.length - 1];
            if (EditorRange.containsRange(parent.range, element.range) && !EditorRange.equalsRange(parent.range, element.range)) {
                if (parent.children) {
                    parent.children.push(element);
                }
                parentStack.push(element);
                break;
            }
            parentStack.pop();
        }
    }
    return res;
}



export function convertToDefinitionLinks(value: cdecode.Definition): any[] {
    if (Array.isArray(value)) {
        return (value as (cdecode.DefinitionLink | cdecode.Location)[]).map(typeConvert.DefinitionLink.from);
    } else if (value) {
        return [typeConvert.DefinitionLink.from(value)];
    }
    return undefined;
}

export function toDocumentSymbols(value: cdecode.SymbolInformation[] | DocumentSymbol[]) {
    if (isFalsyOrEmpty(value)) {
        return undefined;
    }
    if (value[0] instanceof DocumentSymbol) {
        return (<DocumentSymbol[]>value).map(typeConvert.DocumentSymbol.from);
    } else {
        return _asDocumentSymbolTree(<cdecode.SymbolInformation[]>value);
    }
}

export function toCodeLength(symbol: cdecode.CodeLens) {
    // const lens = this._heapService.get<vscode.CodeLens>(ObjectIdentifier.of(symbol));
    // 	if (!lens) {
    // 		return Promise.resolve(undefined);
    // 	}

    // 	let resolve: Promise<vscode.CodeLens | undefined | null>;
    // 	if (typeof this._provider.resolveCodeLens !== 'function' || lens.isResolved) {
    // 		resolve = Promise.resolve(lens);
    // 	} else {
    // 		resolve = asPromise(() => this._provider.resolveCodeLens!(lens, token));
    // 	}

    // 	return resolve.then(newLens => {
    // 		newLens = newLens || lens;
    // 		symbol.command = this._commands.toInternal(newLens.command || CodeLensAdapter._badCmd);
    // 		return symbol;
    // 	});
    return symbol;
}

export function _transformDocumentSelector(selector: cdecode.DocumentSelector): ISerializedDocumentFilter[] {
    if (Array.isArray(selector)) {
        return selector.map(sel => _doTransformDocumentSelector(sel));
    }

    return [_doTransformDocumentSelector(selector)];
}

export function _doTransformDocumentSelector(selector: string | cdecode.DocumentFilter): ISerializedDocumentFilter {
    if (typeof selector === 'string') {
        return {
            $serialized: true,
            language: selector,
        };
    }

    if (selector) {
        return {
            $serialized: true,
            language: selector.language,
            scheme: _transformScheme(selector.scheme),
            pattern: selector.pattern,
            exclusive: selector.exclusive,
        };
    }

    return undefined;
}

export function _transformScheme(scheme: string, _schemeTransformer?: ISchemeTransformer): string {
    if (_schemeTransformer && typeof scheme === 'string') {
        return _schemeTransformer.transformOutgoing(scheme);
    }
    return scheme;
}

export function hoverAdapter(value: cdecode.Hover, doc: cdecode.TextDocument, pos: Position) {
    if (!value || isFalsyOrEmpty(value.contents)) {
        return undefined;
    }
    // if (!value.range) {
    //     value.range = doc.getWordRangeAtPosition(pos);
    // }
    console.log('---todo---still not reading hoverAdapter');
    if (!value.range) {
        value.range = new Range(pos, pos);
    }
    const result = typeConvert.Hover.from(value);
    console.log('HOver adapter  ', result);
    return result;
}

export function toDocumentHighlights(value: cdecode.DocumentHighlight[]) {
    if (Array.isArray(value)) {
        return value.map(typeConvert.DocumentHighlight.from);
    }
    return undefined;
}

export function toReferences(value: cdecode.Location[]) {
    if (Array.isArray(value)) {
        return value.map(typeConvert.location.from);
    }
    return undefined;
}

export function toCodeLens(lenses: cdecode.CodeLens[]): modes.ICodeLensSymbol[] {
    if (Array.isArray(lenses)) {
        return lenses.map(lens => {
            // const id = this._heapService.keep(lens);
            // return ObjectIdentifier.mixin({
            // range: typeConvert.Range.from(lens.range),
            // command: this._commands.toInternal(lens.command),
            // }, id);
        }) as any;
    }
    return undefined;
}

export function CodeActionAdapter_isCommand(thing: any): thing is cdecode.Command {
    return typeof (<cdecode.Command>thing).command === 'string' && typeof (<cdecode.Command>thing).title === 'string';
}

export function toTextEdit(value: cdecode.TextEdit[]) {
    if (Array.isArray(value)) {
        return value.map(typeConvert.TextEdit.from);
    }
}

export function toWorkspaceEdit(value: cdecode.WorkspaceEdit) {

    if (!value) {
        return undefined;
    }
    return typeConvert.WorkspaceEdit.from(value);
}

export function _asMessage(err: any): string {
    if (typeof err === 'string') {
        return err;
    } else if (err instanceof Error && typeof err.message === 'string') {
        return err.message;
    } else {
        return undefined;
    }
}
export function RenameAdapter_resolveRenameLocation(rangeOrLocation: cdecode.Range | { range: cdecode.Range, placeholder: string }, doc: cdecode.TextDocument, pos: Position) {
    // tslint:disable-next-line:no-shadowed-variable
    let range: cdecode.Range;
    let text: string;
    if (Range.isRange(rangeOrLocation)) {
        range = rangeOrLocation;
        text = doc.getText(rangeOrLocation);

    } else if (isObject(rangeOrLocation)) {
        range = rangeOrLocation.range;
        text = rangeOrLocation.placeholder;
    }

    if (!range) {
        return undefined;
    }
    if (range.start.line > pos.line || range.end.line < pos.line) {
        console.warn('INVALID rename location: position line must be within range start/end lines');
        return undefined;
    }
    return { range: typeConvert.Range.from(range), text };
}


export function toCompletionItem(value: cdecode.CompletionItem[] | cdecode.CompletionList, doc: cdecode.TextDocument, pos: Position) {
    //this not exist
    const _id = 0; //this._idPool++;

    const result: SuggestResultDto = {
        _id,
        suggestions: [],
    };


    let list: CompletionList;
    if (!value) {
        // undefined and null are valid results
        return undefined;

    } else if (Array.isArray(value)) {
        list = new CompletionList(value);

    } else {
        list = value;
        result.incomplete = list.isIncomplete;
    }

    // the default text edit range
    const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) as Range || new Range(pos, pos))
        .with({ end: pos });

    for (let i = 0; i < list.items.length; i++) {
        const suggestion = this._convertCompletionItem(list.items[i], pos, wordRangeBeforePos, i, _id);
        // check for bad completion item
        // for the converter did warn
        if (suggestion) {
            result.suggestions.push(suggestion);
        }
    }
    // this._cache.set(_id, list.items);

    return result;
}

export function toCodeActions(commandsOrActions: (cdecode.Command | cdecode.CodeAction)[], codeActionContext: cdecode.CodeActionContext) {
    if (isFalsyOrEmpty(commandsOrActions)) {
        return undefined;
    }
    const result: CustomCodeAction[] = [];
    for (const candidate of commandsOrActions) {
        if (!candidate) {
            continue;
        }
        //commented as CodeActionAdapter is server class
        if (CodeActionAdapter_isCommand(candidate)) {
            // old school: synthetic code action
            result.push({
                _isSynthetic: true,
                title: candidate.title,
                command: this._commands.toInternal(candidate),
            });
        } else {
            if (codeActionContext.only) {
                if (!candidate.kind) {
                    logger.warn(`${this._extensionId} - Code actions of kind '${codeActionContext.only.value} 'requested but returned code action does not have a 'kind'.
                    Code action will be dropped. Please set 'CodeAction.kind'.`);
                } else if (!codeActionContext.only.contains(candidate.kind)) {
                    logger.warn(`${this._extensionId} -Code actions of kind '${codeActionContext.only.value} 'requested but returned code action is of kind '${candidate.kind.value}'.
                    Code action will be dropped.
                    Please check 'CodeActionContext.only' to only return requested code actions.`);
                }
            }

            // new school: convert code action
            result.push({
                title: candidate.title,
                command: candidate.command && this._commands.toInternal(candidate.command),
                diagnostics: candidate.diagnostics && candidate.diagnostics.map(typeConvert.Diagnostic.from),
                edit: candidate.edit && typeConvert.WorkspaceEdit.from(candidate.edit),
                kind: candidate.kind && candidate.kind.value,
            });
        }
    }
    return result;
}

export function toSignatureHelp(value: cdecode.SignatureHelp) {
    if (value) {
        return typeConvert.SignatureHelp.from(value);
    }
    return undefined;
}

export function toDocumentFormating(value: cdecode.TextEdit[]) {
    if (Array.isArray(value)) {
        return value.map(typeConvert.TextEdit.from);
    }
    return undefined;
}


export function RenameAdapter_supportsResolving(provider: cdecode.RenameProvider): boolean {
    return typeof provider.prepareRename === 'function';
}

export function provideColors(colors): IRawColorInfo[] {
    if (!Array.isArray(colors)) {
        return [];
    }

    const colorInfos: IRawColorInfo[] = colors.map(ci => {
        return {
            color: typeConvert.Color.from(ci.color),
            range: typeConvert.Range.from(ci.range),
        };
    });

    return colorInfos;
}

export function provideColorPresentations(value: cdecode.ColorPresentation[]): modes.IColorPresentation[] | undefined {
    if (!Array.isArray(value)) {
        return undefined;
    }
    return value.map(typeConvert.ColorPresentation.from);
}

export function provideFoldingRanges(ranges: cdecode.FoldingRange[]): modes.FoldingRange[] | undefined {
    if (!Array.isArray(ranges)) {
        return undefined;
    }
    return ranges.map(typeConvert.FoldingRange.from);
}
//=============================
export function provideWorkspaceSymbols(value: cdecode.SymbolInformation[]): WorkspaceSymbolsDto {
    const result: WorkspaceSymbolsDto = IdObject.mixin({ symbols: [] });
    if (isNonEmptyArray(value)) {
        for (const item of value) {
            if (!item) {
                // drop
                continue;
            }
            if (!item.name) {
                console.warn('INVALID SymbolInformation, lacks name', item);
                continue;
            }
            const symbol = IdObject.mixin(typeConvert.WorkspaceSymbol.from(item as any));
            this._symbolCache[symbol._id!] = item;
            result.symbols.push(symbol as any);
        }
    }
    return result;
}

export function resolveWorkspaceSymbol(value, symbol: cdecode.SymbolInformation): WorkspaceSymbolDto | undefined {
    return value && mixin(symbol, typeConvert.WorkspaceSymbol.from(value), true);
}

export function releaseWorkspaceSymbols(id: number): any {
    // tslint:disable-next-line:no-shadowed-variable
    const range = this._resultCache[id];
    if (range) {
        for (let [from, to] = range; from <= to; from++) {
            delete this._symbolCache[from];
        }
        delete this._resultCache[id];
    }
}
//=============================

export class SuggestAdapter {

    private _cache = new Map<number, cdecode.CompletionItem[]>();
    private _idPool = 0;

    public provideCompletionItems = (value: cdecode.CompletionItem[] | cdecode.CompletionList, doc: cdecode.TextDocument, pos: Position): SuggestResultDto => {
        const _id = this._idPool++;

        const result: SuggestResultDto = {
            _id,
            suggestions: [],
        };

        let list: CompletionList;
        if (!value) {
            // undefined and null are valid results
            return undefined;

        } else if (Array.isArray(value)) {
            list = new CompletionList(value);

        } else {
            list = value;
            result.incomplete = list.isIncomplete;
        }

        // the default text edit range
        const wordRangeBeforePos = doc.getWordRangeAtPosition ? (doc.getWordRangeAtPosition(pos) as Range || new Range(pos, pos))
            .with({ end: pos }) : null;

        for (let i = 0; i < list.items.length; i++) {
            const suggestion = this._convertCompletionItem(list.items[i], pos, wordRangeBeforePos, i, _id);
            // check for bad completion item
            // for the converter did warn
            if (suggestion) {
                result.suggestions.push(suggestion);
            }
        }
        this._cache.set(_id, list.items);

        return result;
    }

    private _convertCompletionItem = (item: cdecode.CompletionItem, position: cdecode.Position, defaultRange: cdecode.Range, _id: number, _parentId: number): SuggestionDto => {
        if (typeof item.label !== 'string' || item.label.length === 0) {
            console.warn('INVALID text edit -> must have at least a label');
            return undefined;
        }

        const result: SuggestionDto = {
            //
            _id,
            _parentId,
            //
            label: item.label,
            kind: typeConvert.CompletionItemKind.from(item.kind),
            detail: item.detail,
            documentation: item.documentation,
            filterText: item.filterText,
            sortText: item.sortText,
            preselect: item.preselect,
            //
            range: undefined,
            insertText: undefined,
            insertTextRules: typeConvert.CompletionItemInsertTextRule.from(item.insertTextRules),
            additionalTextEdits: item.additionalTextEdits && item.additionalTextEdits.map(typeConvert.TextEdit.from),
            command: {
                id: item.command.command,
                title: item.command.title,
            },
            commitCharacters: item.commitCharacters,
            // help with perf
            // _labelLow: item.label.toLowerCase(),
            // _filterTextLow: item.filterText && item.filterText.toLowerCase(),
            // _sortTextLow: item.sortText && item.sortText.toLowerCase()
        };

        // 'insertText'-logic
        if (item.textEdit) {
            result.insertText = item.textEdit.newText;

        } else if (typeof item.insertText === 'string') {
            result.insertText = item.insertText;

        } else if (item.insertText instanceof SnippetString) {
            result.insertText = item.insertText.value;
            result.insertTextRules += modes.CompletionItemInsertTextRule.InsertAsSnippet;

        } else {
            result.insertText = item.label;
        }

        // 'overwrite[Before|After]'-logic
        // tslint:disable-next-line:no-shadowed-variable
        let range: cdecode.Range;
        if (item.textEdit) {
            range = item.textEdit.range;
        } else if (item.range) {
            range = item.range;
        } else {
            range = defaultRange;
        }
        result.range = typeConvert.Range.from(range);

        if (!range.isSingleLine || range.start.line !== position.line) {
            console.warn('INVALID text edit -> must be single line and on the same line');
            return undefined;
        }

        return result;
    }

}

export class CodeInsetAdapter {

    private readonly _heapService: ExtHostHeapService;
    constructor() {
        this._heapService = new ExtHostHeapService();
    }

    public provideCodeInsets(insets): CodeInsetDto[] | undefined {
        if (Array.isArray(insets)) {
            return insets.map(inset => {
                const $ident = this._heapService.keep(inset);
                const id = generateUuid();
                return {
                    $ident,
                    id,
                    range: typeConvert.Range.from(inset.range),
                    height: inset.height,
                };
            });
        }
    }

    public resolveCodeInset(symbol: CodeInsetDto): CodeInsetDto {
        return symbol;
    }
}

export class LinkProviderAdapter {

    private readonly _heapService: ExtHostHeapService;
    constructor() {
        this._heapService = new ExtHostHeapService();
    }

    public provideLinks(links): LinkDto[] | undefined {
        if (!Array.isArray(links)) {
            return undefined;
        }
        const result: LinkDto[] = [];
        for (const link of links) {
            const data = typeConvert.DocumentLink.from(link);
            const id = this._heapService.keep(link);
            result.push(ObjectIdentifier.mixin(data, id));
        }
        return result;
    }

    public resolveLink(value: cdecode.DocumentLink): modes.ILink | undefined {
        if (value) {
            return typeConvert.DocumentLink.from(value);
        }
    }

}

