import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, DocListCast, DocListCastAsync, Field } from '../../../fields/Doc';
import { documentSchema } from "../../../fields/documentSchemas";
import { Id } from '../../../fields/FieldSymbols';
import { createSchema, makeInterface } from '../../../fields/Schema';
import { StrCast } from '../../../fields/Types';
import { DocumentType } from "../../documents/DocumentTypes";
import { CollectionDockingView } from "../collections/CollectionDockingView";
import { ViewBoxBaseComponent } from "../DocComponent";
import { FieldView, FieldViewProps } from '../nodes/FieldView';
import "./SearchBox.scss";
import { DocumentManager } from '../../util/DocumentManager';
import { DocUtils } from '../../documents/Documents';
import { Tooltip } from "@material-ui/core";

export const searchSchema = createSchema({
    Document: Doc
});

type SearchBoxDocument = makeInterface<[typeof documentSchema, typeof searchSchema]>;
const SearchBoxDocument = makeInterface(documentSchema, searchSchema);

export interface SearchBoxProps extends FieldViewProps {
    linkSearch: boolean;
    linkFrom?: (() => Doc | undefined) | undefined;
}

/**
 * This is the SearchBox component. It represents the search box input and results in
 * the search panel on the left side of the screen.
 */
@observer
export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDocument>(SearchBoxDocument) {
    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); }
    public static Instance: SearchBox;

    private _inputRef = React.createRef<HTMLInputElement>();

    @observable _searchString = "";
    @observable _docTypeString = "all";
    @observable _results: [Doc, string[]][] = [];
    @observable _selectedResult: Doc | undefined = undefined;
    @observable _deletedDocsStatus: boolean = false;
    @observable _onlyAliases: boolean = true;

    /**
     * This is the constructor for the SearchBox class.
     */
    constructor(props: any) {
        super(props);
        SearchBox.Instance = this;
    }

    /** 
     * This method is called when the SearchBox component is first mounted. When the user opens
     * the search panel, the search input box is automatically selected. This allows the user to
     * type in the search input box immediately, without needing clicking on it first.
     */
    componentDidMount = action(() => {
        if (this._inputRef.current) {
            this._inputRef.current.focus();
        }
    });

    /**
     * This method is called when the SearchBox component is about to be unmounted. When the user
     * closes the search panel, the search and its results are reset.
     */
    componentWillUnmount() {
        this.resetSearch();
    }

    /**
     * This method is called when the text in the search input box is modified by the user. The
     * _searchString is updated to the new value of the text in the input box and submitSearch
     * is called to update the search results accordingly.
     * 
     * (Note: There is no longer a need to press enter to submit a search. Any update to the input
     * causes a search to be submitted automatically.)
     */
    onInputChange = action((e: React.ChangeEvent<HTMLInputElement>) => {
        this._searchString = e.target.value;
        this.submitSearch();
    });

    /**
     * This method is called when the option in the select drop-down menu is changed. The
     * _docTypeString is updated to the new value of the option in the drop-down menu. This
     * is used to filter the results of the search to documents of a specific type.
     * 
     * (Note: This doesn't affect the results array, so there is no need to submit a new
     * search here. The results of the search on the _searchString query are simply filtered
     * by type directly before rendering them.)
     */
    onSelectChange = action((e: React.ChangeEvent<HTMLSelectElement>) => {
        this._docTypeString = e.target.value;
    });

    /**
     * @param {Doc} doc - doc of the search result that has been clicked on
     *
     * This method is called when the user clicks on a search result. The _selectedResult is
     * updated accordingly and the doc is highlighted with the selectElement method.
     */
    onResultClick = action(async (doc: Doc) => {
        this._selectedResult = doc;
        this.selectElement(doc, () => DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.(this._searchString, undefined, false));
    });

    makeLink = action((linkTo: Doc) => {
        console.log(linkTo.title);
        if (this.props.linkFrom) {
            const linkFrom = this.props.linkFrom();
            if (linkFrom) {
                console.log(linkFrom.title);
                DocUtils.MakeLink({ doc: linkFrom }, { doc: linkTo }, "Link");
            }
        }
    });

    /**
     * @param {Doc[]} docs - docs to be searched through recursively
     * @param {number, Doc => void} func - function to be called on each doc
     *
     * This method iterates through an array of docs and all docs within those docs, calling
     * the function func on each doc.
     */
    static foreachRecursiveDoc(docs: Doc[], func: (depth: number, doc: Doc) => void) {
        let newarray: Doc[] = [];
        var depth = 0;
        while (docs.length > 0) {
            newarray = [];
            docs.filter(d => d).forEach(d => {
                const fieldKey = Doc.LayoutFieldKey(d);
                const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView");
                const data = d[annos ? fieldKey + "-annotations" : fieldKey];
                data && newarray.push(...DocListCast(data));
                func(depth, d);
            });
            docs = newarray;
            depth++;
        }
    }

    /**
     * @param {Doc[]} docs - docs to be searched through recursively
     * @param {number, Doc => void} func - function to be called on each doc
     *
     * This method iterates asynchronously through an array of docs and all docs within those
     * docs, calling the function func on each doc.
     */
    static async foreachRecursiveDocAsync(docs: Doc[], func: (depth: number, doc: Doc) => void) {
        let newarray: Doc[] = [];
        var depth = 0;
        while (docs.length > 0) {
            newarray = [];
            await Promise.all(docs.filter(d => d).map(async d => {
                const fieldKey = Doc.LayoutFieldKey(d);
                const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView");
                const data = d[annos ? fieldKey + "-annotations" : fieldKey];
                const docs = await DocListCastAsync(data);
                docs && newarray.push(...docs);
                func(depth, d);
            }));
            docs = newarray;
            depth++;
        }
    }

    /**
     * @param {String} type - string representing the type of a doc
     *
     * This method converts a doc type string of any length to a 3-letter doc type string in
     * which the first letter is capitalized. This is used when displaying the type on the
     * right side of each search result.
     */
    static formatType(type: String): String {
        if (type === "pdf") {
            return "PDF";
        }
        else if (type === "image") {
            return "Img";
        }

        return type.charAt(0).toUpperCase() + type.substring(1, 3);
    }

    /**
     * @param {String} query - search query string
     *
     * This method searches the CollectionDockingView instance for a certain query and puts
     * the matching results in the results array. Docs are considered to be matching results
     * when the query is a substring of many different pieces of its metadata (title, text,
     * author, etc).
     */
    @action
    searchCollection(query: string) {
        const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
        const blockedKeys = ["x", "y", "proto", "width", "autoHeight", "acl-Override", "acl-Public", "context", "zIndex", "height", "text-scrollHeight", "text-height", "cloneFieldFilter", "isPrototype", "text-annotations",
            "dragFactory-count", "text-noTemplate", "aliases", "system", "layoutKey", "baseProto", "xMargin", "yMargin", "links", "layout", "layout_keyValue", "fitWidth", "viewType", "title-custom",
            "panX", "panY", "viewScale"];
        const collection = CollectionDockingView.Instance;
        query = query.toLowerCase();

        this._results = [];
        this._selectedResult = undefined;

        if (collection !== undefined) {
            const docs = DocListCast(collection.rootDoc[Doc.LayoutFieldKey(collection.rootDoc)]);
            const docIDs: String[] = [];
            SearchBox.foreachRecursiveDoc(docs, (depth: number, doc: Doc) => {
                const dtype = StrCast(doc.type, "string") as DocumentType;
                if (dtype && !blockedTypes.includes(dtype) && !docIDs.includes(doc[Id]) && depth > 0) {
                    const hlights = new Set<string>();
                    SearchBox.documentKeys(doc).forEach(key => Field.toString(doc[key] as Field).toLowerCase().includes(query) && hlights.add(key));
                    blockedKeys.forEach(key => hlights.delete(key));
                    Array.from(hlights.keys()).length > 0 && this._results.push([doc, Array.from(hlights.keys())]);
                }
                docIDs.push(doc[Id]);
            });
        }
    }

    /**
     * @param {Doc} doc - doc for which keys are returned
     * 
     * This method returns a list of a document doc's keys.
     */
    static documentKeys(doc: Doc) {
        const keys: { [key: string]: boolean } = {};
        Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false));
        return Array.from(Object.keys(keys));
    }

    /**
     * This method submits a search with the _searchString as its query and updates
     * the results array accordingly.
     */
    @action
    submitSearch = async () => {
        this.resetSearch();

        const query = StrCast(this._searchString);
        Doc.SetSearchQuery(query);
        this._results = [];

        if (query) {
            this.searchCollection(query);
        }
    }

    /**
     * This method resets the search by iterating through each result and removing all
     * brushes and highlights. All search matches are cleared as well.
     */
    resetSearch = action(() => {
        this._results.forEach(result => {
            Doc.UnBrushDoc(result[0]);
            Doc.UnHighlightDoc(result[0]);
            Doc.ClearSearchMatches();
        });
    });

    /**
     * @param {Doc} doc - doc to be selected
     * 
     * This method selects a doc by either jumping to it (centering/zooming in on it)
     * or opening it in a new tab.
     */
    selectElement = async (doc: Doc, finishFunc: () => void) => {
        await DocumentManager.Instance.jumpToDocument(doc, true, undefined, undefined, undefined, undefined, undefined, finishFunc);
    }

    /**
     * This method returns a JSX list of the options in the select drop-down menu, which
     * is used to filter the types of documents that appear in the search results.
     */
    @computed
    public get selectOptions() {
        const selectValues = ["all", "rtf", "image", "pdf", "web", "video", "audio", "collection"];

        return selectValues.map(value => <option key={value} value={value}>{SearchBox.formatType(value)}</option>);
    }

    /**
     * This method renders the search input box, select drop-down menu, and search results.
     */
    render() {
        var validResults = 0;

        const isLinkSearch: boolean = this.props.linkSearch;


        const results = this._results.map(result => {
            var className = "searchBox-results-scroll-view-result";

            if (this._selectedResult === result[0]) {
                className += " searchBox-results-scroll-view-result-selected";
            }

            const formattedType = SearchBox.formatType(StrCast(result[0].type));
            const title = result[0].title;

            if (this._docTypeString === "all" || this._docTypeString === result[0].type) {
                validResults++;
                return (
                    <Tooltip key={result[0][Id]} placement={"right"} title={<><div className="dash-tooltip">{title}</div></>}>
                        <div onClick={isLinkSearch ?
                            () => this.makeLink(result[0]) :
                            e => {
                                this.onResultClick(result[0]);
                                e.stopPropagation();
                            }} className={className}>
                            <div className="searchBox-result-title">
                                {title}
                            </div>
                            <div className="searchBox-result-type">
                                {formattedType}
                            </div>
                            <div className="searchBox-result-keys">
                                {result[1].join(", ")}
                            </div>
                        </div>
                    </Tooltip>
                );
            }

            return null;
        });

        results.filter(result => result);

        return (
            <div style={{ pointerEvents: "all" }} className="searchBox-container">
                <div className="searchBox-bar" >
                    {isLinkSearch ? (null) : <select name="type" id="searchBox-type" className="searchBox-type" onChange={this.onSelectChange}>
                        {this.selectOptions}
                    </select>}
                    <input defaultValue={""} autoComplete="off" onChange={this.onInputChange} onKeyPress={e => e.key === "Enter" ? this.submitSearch() : null} type="text" placeholder="Search..." id="search-input" className="searchBox-input" style={{ width: isLinkSearch ? "100%" : undefined, borderRadius: isLinkSearch ? "5px" : undefined }} ref={this._inputRef} />
                </div >
                <div className="searchBox-results-container">
                    <div className="searchBox-results-count">
                        {`${validResults}` + " result" + (validResults === 1 ? "" : "s")}
                    </div>
                    <div className="searchBox-results-scroll-view">
                        {results}
                    </div>
                </div>
            </div >
        );
    }
}