import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { Box, Button, Checkbox, createStyles, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, FormControlLabel, LinearProgress, List, ListItem, ListItemIcon, ListItemText, makeStyles, MenuItem, Paper, Select, Theme, Typography } from "@material-ui/core";
import StoreLinkIcon from '../../common/StoreLinkIcon';
import { $enum } from 'ts-enum-util';
import { IntegrationState, useIntegrations } from '../../../lib/integration/useIntegrations';
import { IntegrationWith, ImplIntegratesWith, IntegrationImpl, ResourceType, QueryResponseType, IntegrationUrls } from '../../../api/api';
import { start } from 'repl';
import { QueryLeafBy, QueryLeafOp, QueryNodeOp, queryNot } from '../../../lib/query/Query';
import { queryAlbums, queryArtists, queryItems, queryTracks } from '../../../lib/backend/queries';
import asyncPool from "tiny-async-pool";
import { getTrack } from '../../../lib/backend/tracks';
import { getAlbum } from '../../../lib/backend/albums';
import { getArtist } from '../../../lib/backend/artists';
import { modifyAlbum, modifyArtist, modifyTrack } from '../../../lib/saveChanges';

const useStyles = makeStyles((theme: Theme) =>
    createStyles({
        disabled: {
            color: theme.palette.text.disabled,
        },
    })
);

enum BatchJobState {
    Idle = 0,
    Collecting,
    Running,
    Finished,
}

interface Task {
    itemType: ResourceType,
    itemId: number,
    integrationId: number,
    store: IntegrationWith,
}

interface BatchJobStatus {
    state: BatchJobState,
    numTasks: number,
    tasksSuccess: number,
    tasksFailed: number,
}

async function makeTasks(
    integration: IntegrationState,
    linkTracks: boolean,
    linkArtists: boolean,
    linkAlbums: boolean,
    addTaskCb: (t: Task) => void,
) {
    let whichProp: any = {
        [ResourceType.Track]: QueryLeafBy.TrackStoreLinks,
        [ResourceType.Artist]: QueryLeafBy.ArtistStoreLinks,
        [ResourceType.Album]: QueryLeafBy.AlbumStoreLinks,
    }
    let whichElem: any = {
        [ResourceType.Track]: 'tracks',
        [ResourceType.Artist]: 'artists',
        [ResourceType.Album]: 'albums',
    }
    let maybeStore = integration.integration.providesStoreLink();
    if (!maybeStore) {
        return;
    }
    let store = maybeStore as IntegrationWith;
    let doForType = async (type: ResourceType) => {
        let ids: number[] = ((await queryItems(
            [type],
            queryNot({
                a: whichProp[type],
                leafOp: QueryLeafOp.Like,
                b: `%${IntegrationUrls[store]}%`,
            }),
            undefined,
            undefined,
            QueryResponseType.Ids
        )) as any)[whichElem[type]];
        ids.map((id: number) => {
            addTaskCb({
                itemType: type,
                itemId: id,
                integrationId: integration.id,
                store: store,
            });
        })
    }
    var promises: Promise<any>[] = [];
    if (linkTracks) { promises.push(doForType(ResourceType.Track)); }
    if (linkArtists) { promises.push(doForType(ResourceType.Artist)); }
    if (linkAlbums) { promises.push(doForType(ResourceType.Album)); }
    await Promise.all(promises);
}

async function doLinking(
    toLink: { integrationId: number, tracks: boolean, artists: boolean, albums: boolean }[],
    setStatus: any,
    integrations: IntegrationState[],
) {
    // Start the collecting phase.
    setStatus((s: any) => {
        return {
            state: BatchJobState.Collecting,
            numTasks: 0,
            tasksSuccess: 0,
            tasksFailed: 0,
        }
    });
    var tasks: Task[] = [];

    let collectionPromises = toLink.map((v: any) => {
        let { integrationId, tracks, artists, albums } = v;
        let integration = integrations.find((i: IntegrationState) => i.id === integrationId);
        if (!integration) { return; }
        return makeTasks(
            integration,
            tracks,
            artists,
            albums,
            (t: Task) => { tasks.push(t) }
        );
    })
    await Promise.all(collectionPromises);
    // Start the linking phase.
    setStatus((status: BatchJobStatus) => {
        return {
            ...status,
            state: BatchJobState.Running,
            numTasks: tasks.length
        }
    });

    let makeJob: (t: Task) => Promise<void> = (t: Task) => {
        let integration = integrations.find((i: IntegrationState) => i.id === t.integrationId);
        return (async () => {
            let onSuccess = () =>
                setStatus((s: BatchJobStatus) => {
                    return {
                        ...s,
                        tasksSuccess: s.tasksSuccess + 1,
                    }
                });
            let onFail = () =>
                setStatus((s: BatchJobStatus) => {
                    return {
                        ...s,
                        tasksFailed: s.tasksFailed + 1,
                    }
                });
            try {
                if (integration === undefined) { return; }
                let _integration = integration as IntegrationState;
                let searchFuncs: any = {
                    [ResourceType.Track]: (q: any, l: any) => { return _integration.integration.searchTrack(q, l) },
                    [ResourceType.Album]: (q: any, l: any) => { return _integration.integration.searchAlbum(q, l) },
                    [ResourceType.Artist]: (q: any, l: any) => { return _integration.integration.searchArtist(q, l) },
                }
                // TODO include related items in search
                let getFuncs: any = {
                    [ResourceType.Track]: getTrack,
                    [ResourceType.Album]: getAlbum,
                    [ResourceType.Artist]: getArtist,
                }
                let queryFuncs: any = {
                    [ResourceType.Track]: (s: any) => `${s.name}` +
                        `${s.artists && s.artists.length > 0 && ` ${s.artists[0].name}` || ''}` +
                        `${s.albums && s.albums.length > 0 && ` ${s.albums[0].name}` || ''}`,
                    [ResourceType.Album]: (s: any) => `${s.name}` +
                        `${s.artists && s.artists.length > 0 && ` ${s.artists[0].name}` || ''}`,
                    [ResourceType.Artist]: (s: any) => `${s.name}`,
                }
                let modifyFuncs: any = {
                    [ResourceType.Track]: modifyTrack,
                    [ResourceType.Album]: modifyAlbum,
                    [ResourceType.Artist]: modifyArtist,
                }
                let item = await getFuncs[t.itemType](t.itemId);
                let query = queryFuncs[t.itemType](item);
                let candidates = await searchFuncs[t.itemType](
                    query,
                    1,
                );

                var success = false;
                if (candidates && candidates.length && candidates.length > 0 && candidates[0].url) {
                    await modifyFuncs[t.itemType](
                        t.itemId,
                        {
                            mbApi_typename: t.itemType,
                            storeLinks: [...item.storeLinks, candidates[0].url],
                        }
                    )
                    success = true;
                }

                if (success) {
                    onSuccess();
                } else {
                    onFail();
                }
            } catch (e) {
                // Report fail
                console.log("Error fetching candidates: ", e)
                onFail();
            }
        })();
    }

    await asyncPool(8, tasks, makeJob);

    // Finalize.
    setStatus((status: BatchJobStatus) => {
        return {
            ...status,
            state: BatchJobState.Finished,
        }
    });
}

function ProgressDialog(props: {
    open: boolean,
    onClose: () => void,
    status: BatchJobStatus,
}) {
    let donePercent = ((props.status.tasksFailed + props.status.tasksSuccess) / (props.status.numTasks || 1)) * 100;
    return <Dialog
        open={props.open}
        onClose={props.onClose}
    >
        {props.status.state === BatchJobState.Collecting || props.status.state === BatchJobState.Running &&
            <DialogTitle>Batch linking in progress...</DialogTitle>}
        {props.status.state === BatchJobState.Finished &&
            <DialogTitle>Batch linking finished</DialogTitle>}
        <DialogContent>
            {props.status.state === BatchJobState.Collecting || props.status.state === BatchJobState.Running &&
                <DialogContentText>
                    Closing or refreshing this page will interrupt and abort the process.
        </DialogContentText>}

            <Box minWidth="200px"><LinearProgress variant="determinate" color="secondary" value={donePercent} /></Box>
            <Typography>
                Found: {props.status.tasksSuccess}<br />
                Failed: {props.status.tasksFailed}<br />
                Total: {props.status.numTasks}<br />
            </Typography>
        </DialogContent>
        {props.status.state === BatchJobState.Finished && <DialogActions>
            <Button variant="contained" onClick={props.onClose}>Done</Button>
        </DialogActions>}
    </Dialog>
}

function ConfirmDialog(props: {
    open: boolean
    onConfirm: () => void,
    onClose: () => void,
}) {
    return <Dialog
        open={props.open}
        onClose={props.onClose}
    >
        <DialogTitle>Are you sure?</DialogTitle>
        <DialogContent>
            <DialogContentText>
                This action is non-reversible.
            </DialogContentText>
        </DialogContent>
        <DialogActions>
            <Button onClick={props.onClose} variant="outlined">Cancel</Button>
            <Button onClick={() => { props.onClose(); props.onConfirm(); }} variant="contained">Confirm</Button>
        </DialogActions>
    </Dialog>
}

export default function BatchLinkDialog(props: {
    open: boolean,
    onClose: () => void,
}) {
    let integrations = useIntegrations();
    let classes = useStyles();
    let [confirmDialogOpen, setConfirmDialogOpen] = useState<boolean>(false);
    let [jobStatus, setJobStatus] = useState<BatchJobStatus>({
        state: BatchJobState.Idle,
        numTasks: 0,
        tasksSuccess: 0,
        tasksFailed: 0,
    });

    var compatibleIntegrations: Record<IntegrationWith, IntegrationState[]> = {
        [IntegrationWith.GooglePlayMusic]: [],
        [IntegrationWith.YoutubeMusic]: [],
        [IntegrationWith.Spotify]: [],
    };
    $enum(IntegrationWith).getValues().forEach((store: IntegrationWith) => {
        compatibleIntegrations[store] = Array.isArray(integrations.state) ?
            integrations.state.filter((i: IntegrationState) => ImplIntegratesWith[i.properties.type] === store)
            : [];
    })

    interface StoreSettings {
        selectedIntegration: number | undefined, // Index into compatibleIntegrations
        linkArtists: boolean,
        linkTracks: boolean,
        linkAlbums: boolean,
    }

    let [storeSettings, setStoreSettings] = useState<Record<IntegrationWith, StoreSettings>>(
        $enum(IntegrationWith).getValues().reduce((prev: any, cur: IntegrationWith) => {
            return {
                ...prev,
                [cur]: {
                    selectedIntegration: compatibleIntegrations[cur].length > 0 ? 0 : undefined,
                    linkArtists: false,
                    linkTracks: false,
                    linkAlbums: false,
                }
            }
        }, {})
    );

    let Text = (props: any) => {
        return props.enabled ? <Typography>{props.children}</Typography> :
            <Typography className={classes.disabled}>{props.children}</Typography>
    }

    return <>
        <Dialog
            maxWidth="md"
            fullWidth
            open={props.open}
            onClose={props.onClose}
            disableBackdropClick={true}>
            <Box m={2}>
                <Typography variant="h5">Batch linking</Typography>
                <Typography>
                    Using this feature, links to external websites will automatically be added to existing items
                in your music library. This happens by using any available integrations you have configured.<br />
                Existing links are untouched.
        </Typography>
                <table style={{ borderSpacing: "20px" }}>
                    <tr>
                        <th><Typography><b>Service</b></Typography></th>
                        <th><Typography><b>Use Integration</b></Typography></th>
                        <td><Typography><b>Which items</b></Typography></td>
                    </tr>
                    {$enum(IntegrationWith).getValues().map((store: IntegrationWith) => {
                        let active = Boolean(compatibleIntegrations[store].length);

                        return <tr>
                            <td>
                                <Box display="flex" alignItems="center" flexDirection="column">
                                    <StoreLinkIcon whichStore={store} />
                                    <Text enabled={active}>{store}</Text>
                                </Box>
                            </td>
                            <td>
                                {!active && <Text enabled={active}>No integrations configured.</Text>}
                                {active && <Select fullWidth
                                    value={storeSettings[store].selectedIntegration}
                                    onChange={(e: any) => {
                                        setStoreSettings({
                                            ...storeSettings,
                                            [store]: {
                                                ...storeSettings[store],
                                                selectedIntegration: e.target.value,
                                            }
                                        })
                                    }}
                                >
                                    {compatibleIntegrations[store].map((c: IntegrationState, idx: number) => {
                                        return <MenuItem value={idx}>{c.properties.name}</MenuItem>
                                    })}
                                </Select>}
                            </td>
                            <td>
                                <FormControlLabel control={
                                    <Checkbox disabled={!active} checked={storeSettings[store].linkArtists}
                                        onChange={(e: any) => setStoreSettings((prev: any) => { return { ...prev, [store]: { ...prev[store], linkArtists: e.target.checked } } })} />
                                } label={<Text enabled={active}>Artists</Text>} />
                                <FormControlLabel control={
                                    <Checkbox disabled={!active} checked={storeSettings[store].linkAlbums}
                                        onChange={(e: any) => setStoreSettings((prev: any) => { return { ...prev, [store]: { ...prev[store], linkAlbums: e.target.checked } } })} />
                                } label={<Text enabled={active}>Albums</Text>} />
                                <FormControlLabel control={
                                    <Checkbox disabled={!active} checked={storeSettings[store].linkTracks}
                                        onChange={(e: any) => setStoreSettings((prev: any) => { return { ...prev, [store]: { ...prev[store], linkTracks: e.target.checked } } })} />
                                } label={<Text enabled={active}>Tracks</Text>} />
                            </td>
                        </tr>;
                    })}
                </table>
                <DialogActions>
                    <Button variant="outlined"
                        onClick={props.onClose}>Cancel</Button>
                    <Button variant="contained" color="secondary"
                        onClick={() => setConfirmDialogOpen(true)}>Start</Button>
                </DialogActions>
            </Box>
        </Dialog>
        <ConfirmDialog
            open={confirmDialogOpen}
            onClose={() => setConfirmDialogOpen(false)}
            onConfirm={() => {
                var toLink: any[] = [];
                Object.keys(storeSettings).forEach((store: string) => {
                    let s = store as IntegrationWith;
                    let active = Boolean(compatibleIntegrations[s].length);

                    if (active && storeSettings[s].selectedIntegration !== undefined) {
                        toLink.push({
                            integrationId: compatibleIntegrations[s][storeSettings[s].selectedIntegration || 0].id,
                            tracks: storeSettings[s].linkTracks,
                            artists: storeSettings[s].linkArtists,
                            albums: storeSettings[s].linkAlbums,
                        });
                    }
                });
                doLinking(
                    toLink,
                    setJobStatus,
                    integrations.state === "Loading" ?
                        [] : integrations.state,
                )
            }}
        />
        <ProgressDialog
            open={jobStatus.state === BatchJobState.Collecting || jobStatus.state === BatchJobState.Running || jobStatus.state === BatchJobState.Finished}
            onClose={() => {
                setJobStatus({
                    numTasks: 0,
                    tasksSuccess: 0,
                    tasksFailed: 0,
                    state: BatchJobState.Idle,
                })
            }}
            status={jobStatus}
        />
    </>
}