import React, { useEffect, useReducer, useCallback } from 'react';
import { Box, LinearProgress, Typography } from '@material-ui/core';
import { QueryElem, QueryLeafBy, QueryLeafElem, QueryLeafOp } from '../../../lib/query/Query';
import QueryBuilder from '../../querybuilder/QueryBuilder';
import { AlbumsTable, ArtistsTable, ColumnType, ItemsTable, TracksTable } from '../../tables/ResultsTable';
import { queryArtists, queryTracks, queryAlbums, queryTags } from '../../../lib/backend/queries';
import { WindowState } from '../Windows';
import { QueryResponseType, QueryResponseAlbumDetails, QueryResponseTagDetails, QueryResponseArtistDetails, QueryResponseTrackDetails, Artist, Name } from '../../../api/api';
import { ServerStreamResponseOptions } from 'http2';
import { TrackChangesSharp } from '@material-ui/icons';
import { v4 as genUuid } from 'uuid';
import stringifyList from '../../../lib/stringifyList';
var _ = require('lodash');

export enum QueryItemType {
    Artists,
    Tracks,
    Albums,
    Tags,
};

export interface ResultsForQuery {
    kind: QueryItemType,
    results: (
        QueryResponseAlbumDetails[] |
        QueryResponseArtistDetails[] |
        QueryResponseTagDetails[] |
        QueryResponseTrackDetails[]
    ),
}

export interface QueryWindowState extends WindowState {
    editingQuery: boolean, // Is the editor in "edit mode"
    query: QueryElem | null, // The actual on-screen query

    includeTypes: QueryItemType[], // which item types do we actually request results for?

    // Whenever queries change, new requests are fired to the server.
    // Each request gets a unique id hash.
    // In this results record, we store the query IDs which
    // we want to show results for.
    resultsForQueries: Record<string, ResultsForQuery | null>;
}

export enum QueryWindowStateActions {
    FiredNewQueries = "firedNewQueries",
    SetEditingQuery = "setEditingQuery",
    ReceivedResult = "receivedResult",
}

async function getArtistNames(filter: string) {
    const artists: any = await queryArtists(
        filter.length > 0 ? {
            a: QueryLeafBy.ArtistName,
            b: '%' + filter + '%',
            leafOp: QueryLeafOp.Like
        } : undefined,
        0, -1, QueryResponseType.Details
    );

    return [...(new Set([...(artists.map((a: any) => a.name))]))];
}

async function getAlbumNames(filter: string) {
    const albums: any = await queryAlbums(
        filter.length > 0 ? {
            a: QueryLeafBy.AlbumName,
            b: '%' + filter + '%',
            leafOp: QueryLeafOp.Like
        } : undefined,
        0, -1, QueryResponseType.Details
    );

    return [...(new Set([...(albums.map((a: any) => a.name))]))];
}

async function getTrackNames(filter: string) {
    const tracks: any = await queryTracks(
        filter.length > 0 ? {
            a: QueryLeafBy.TrackName,
            b: '%' + filter + '%',
            leafOp: QueryLeafOp.Like
        } : undefined,
        0, -1, QueryResponseType.Details
    );

    return [...(new Set([...(tracks.map((s: any) => s.name))]))];
}

async function getTagItems(): Promise<any> {
    let tags: any = await queryTags(
        undefined,
        0, -1, QueryResponseType.Details
    );

    return tags;
}

export interface FireNewQueriesData {
    query: QueryElem | null,
    includeTypes: QueryItemType[],
    resultIds: string[],
}

export interface ReceivedResultData {
    result: ResultsForQuery,
    id: string,
}

export function QueryWindowReducer(state: QueryWindowState, action: any) {
    switch (action.type) {
        case QueryWindowStateActions.ReceivedResult:
            var arr = action.value as ReceivedResultData;
            if (Object.keys(state.resultsForQueries).includes(arr.id)) {
                //console.log("Storing result:", arr);
                var _n = _.cloneDeep(state);
                _n.resultsForQueries[arr.id] = arr.result;
                return _n;
            }
            //console.log("Discarding result:", arr);
            return state;
        case QueryWindowStateActions.FiredNewQueries:
            var newState: QueryWindowState = _.cloneDeep(state);
            let _action = action.value as FireNewQueriesData;
            // Invalidate results
            newState.resultsForQueries = {};
            // Add a null result for each of the new IDs.
            // Results will be added in as they come.
            _action.resultIds && _action.resultIds.forEach((r: string) => {
                newState.resultsForQueries[r] = null;
            })
            newState.query = _action.query;
            newState.includeTypes = _action.includeTypes;
            return newState;
        case QueryWindowStateActions.SetEditingQuery:
            return { ...state, editingQuery: action.value }
        default:
            throw new Error("Unimplemented QueryWindow state update.")
    }
}

export default function QueryWindow(props: {}) {
    const [state, dispatch] = useReducer(QueryWindowReducer, {
        editingQuery: false,
        query: null,
        resultsForQueries: {},
        includeTypes: [QueryItemType.Tracks, QueryItemType.Artists, QueryItemType.Albums, QueryItemType.Tags],
    });

    return <QueryWindowControlled state={state} dispatch={dispatch} />
}

export function QueryWindowControlled(props: {
    state: QueryWindowState,
    dispatch: (action: any) => void,
}) {
    let { query, editingQuery, resultsForQueries, includeTypes } = props.state;
    let { dispatch } = props;

    // Call this function to fire new queries and prepare to receive their results.
    // This will also set the query into the window state.
    const doQueries = async (_query: QueryElem | null, itemTypes: QueryItemType[]) => {
        var promises: Promise<any>[] = [];
        var ids: string[] = itemTypes.map((i: any) => genUuid());
        var query_fns = {
            [QueryItemType.Albums]: queryAlbums,
            [QueryItemType.Artists]: queryArtists,
            [QueryItemType.Tracks]: queryTracks,
            [QueryItemType.Tags]: queryTags,
        };

        let stateUpdateData: FireNewQueriesData = {
            query: _query,
            includeTypes: itemTypes,
            resultIds: ids
        };

        // First dispatch to the state that we are firing new queries.
        // This will update the query on the window page and invalidate
        // any previous results on-screen.
        dispatch({
            type: QueryWindowStateActions.FiredNewQueries,
            value: stateUpdateData
        })

        if (_query) {
            itemTypes.forEach((itemType: QueryItemType, idx: number) => {
                (promises as any[]).push(
                    (async () => {
                        let results = (await query_fns[itemType](
                            _query,
                            0, // TODO: pagination
                            100,
                            QueryResponseType.Details
                        )) as (
                                QueryResponseAlbumDetails[] |
                                QueryResponseArtistDetails[] |
                                QueryResponseTagDetails[] |
                                QueryResponseTrackDetails[]);

                        let r: ReceivedResultData = {
                            id: ids[idx],
                            result: {
                                kind: itemType,
                                results: results
                            }
                        };

                        dispatch({ type: QueryWindowStateActions.ReceivedResult, value: r })
                    })()
                );
            })
        }

        await Promise.all(promises);
    };

    let setEditingQuery = (e: boolean) => {
        props.dispatch({ type: QueryWindowStateActions.SetEditingQuery, value: e });
    }

    return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
        <Box
            m={1}
            width="80%"
        >
            <QueryBuilder
                query={query}
                onChangeQuery={(q: QueryElem | null) => {
                    doQueries(q, includeTypes)
                }}
                editing={editingQuery}
                onChangeEditing={setEditingQuery}
                requestFunctions={{
                    getArtists: getArtistNames,
                    getTrackNames: getTrackNames,
                    getAlbums: getAlbumNames,
                    getTags: getTagItems,
                }}
            />
        </Box>
        <Box
            m={1}
            width="80%"
        >
            {(() => {
                var rr = Object.values(resultsForQueries);
                rr = rr.sort((r: ResultsForQuery | null) => {
                    if (r === null) { return 99; }
                    return {
                        [QueryItemType.Tracks]: 0,
                        [QueryItemType.Albums]: 1,
                        [QueryItemType.Artists]: 2,
                        [QueryItemType.Tags]: 3
                    }[r.kind];
                });
                // TODO: the sorting is not working
                return rr.map((r: ResultsForQuery | null) => <>
                    {r !== null && r.kind == QueryItemType.Tracks && <>
                        <Typography variant="h5">Tracks</Typography>
                        <TracksTable tracks={r.results as QueryResponseTrackDetails[]}/>
                    </>}
                    {r !== null && r.kind == QueryItemType.Albums && <>
                        <Typography variant="h5">Albums</Typography>
                        <AlbumsTable albums={r.results as QueryResponseAlbumDetails[]}/>
                    </>}
                    {r !== null && r.kind == QueryItemType.Artists && <>
                        <Typography variant="h5">Artists</Typography>
                        <ArtistsTable artists={r.results as QueryResponseArtistDetails[]}/>
                    </>}
                    {r !== null && r.kind == QueryItemType.Tags && <>
                        <Typography variant="h5">Tags</Typography>
                        <Typography>Found {r.results.length} tags.</Typography>
                    </>}
                    {r === null && <LinearProgress />}
                </>);
            })()}
        </Box>
    </Box>
}