import './styles.css';

import { createContext, createSignal, createMemo, createEffect, useContext, children, on, onCleanup, Show, For, Switch, Match, createRoot } from 'solid-js';
import { canUseDocument, componentBuilder, extendPropsFrom, mergeClass, getAbsoluteBoundingRect, mergeCallbacks } from '@terraprisma/utils';
import { generateStyleVariablesFrom, addAccentColoring } from '@terraprisma/core';
import { Portal, insert } from 'solid-js/web';
import { createStore, produce } from 'solid-js/store';
import { Collapse, Fade } from '@terraprisma/transitions';
import { KeyboardArrowDown } from '@terraprisma/icons';
import { Divisor } from '@terraprisma/layout';

const TerraprismaContext = createContext();
const getStylesFromTheme = (theme) => {
    let obj;
    if ('mainAccent' in theme && 'accents' in theme) {
        const { id, mainAccent, ...objWithStyles } = {
            ...theme
        };
        obj = {
            ...objWithStyles,
            accent: objWithStyles.accents[mainAccent]
        };
    }
    else {
        const { id, ...objWithStyles } = { ...theme };
        obj = objWithStyles;
    }
    return generateStyleVariablesFrom(obj);
};
/**
 * A component that creates Terraprisma's global context with the themes defined and creates
 * the signal that will hold the current theme.
 *
 * If there are no custom themes defined, Terraprisma's default themes are used.
 *
 * @param defaultThemeId The theme that is going to be used by default - defaults to the first theme in {@link themes}
 */
function setupTerraprisma(themes, // should themes be reactive as well?
defaultThemeId = themes[0].id) {
    const initialTheme = themes.find((t) => t.id === defaultThemeId);
    const initialStyles = getStylesFromTheme(initialTheme);
    return [
        (props) => {
            const [themeId, setThemeId] = createSignal(defaultThemeId);
            const currentTheme = createMemo(() => themes.find((t) => t.id === themeId()));
            const currentGlobalStyles = createMemo(() => getStylesFromTheme(currentTheme()));
            createEffect(() => {
                if (canUseDocument()) {
                    const styles = currentGlobalStyles();
                    Object.keys(styles).forEach((key) => document.body.style.setProperty(key, styles[key]));
                }
            });
            return (<TerraprismaContext.Provider value={{
                    themes,
                    currentTheme,
                    themeId,
                    setThemeId
                }}>
          {props.children}
        </TerraprismaContext.Provider>);
        },
        initialStyles
    ];
}
/**
 * @description Gets access to the global metadata regarding the current theme
 * of the website and available themes.
 *
 * This is supposed to be used inside a `<ThemeProvider>` component since it is the one whom
 * initializes the global Terraprisma context.
 */
function useTheme() {
    const providerValue = useContext(TerraprismaContext);
    if (typeof providerValue === 'undefined') {
        throw new Error('useTheme() error: You can only have a useTheme inside of a Terraprisma context!');
    }
    return providerValue;
}



const Ripple = componentBuilder()
    .factory(addAccentColoring())
    .factory(extendPropsFrom([
    'noRipple',
    'center',
    'color',
    'contrastWithBg',
    'wrapperProps',
    'children'
]))
    .create((props, color, elProps) => {
    const [ripples, setRipples] = createStore([]);
    const createRipple = (element, globalPositionX, globalPositionY) => {
        if (props.noRipple === true)
            return;
        const diameter = Math.max(element.clientWidth, element.clientHeight);
        const radius = diameter / 2;
        const elDOMRect = element.getBoundingClientRect();
        let positionX = globalPositionX - elDOMRect.x;
        let positionY = globalPositionY - elDOMRect.y;
        if (props.center) {
            positionX = radius;
            positionY = radius;
        }
        const rippleConfig = {
            diameter,
            left: positionX - radius,
            top: positionY - radius
        };
        setRipples(produce((ripples) => {
            ripples.unshift(rippleConfig);
            setTimeout(() => {
                ripples.splice(-1, 1);
            }, 2000);
        }));
    };
    const [rippleContainer, setRippleContainer] = createSignal();
    const children$1 = children(() => props.children);
    const childrenList = createMemo(() => {
        const childrenAccessed = children$1();
        return Array.isArray(childrenAccessed)
            ? childrenAccessed
            : [childrenAccessed];
    });
    const [rippledElementBoundingBox, setRippledElementBoundingBox] = createSignal();
    const updateRipplesPosition = () => setRippledElementBoundingBox(getAbsoluteBoundingRect(rippledElement()));
    const addNewRipple = (event) => {
        updateRipplesPosition();
        createRipple(rippleContainer(), event.x, event.y);
    };
    let isUpdatingRippleWrapper = false;
    const keepUpdatingRippleWrapper = () => {
        isUpdatingRippleWrapper = true;
        requestAnimationFrame(() => {
            if (ripples.length > 0) {
                updateRipplesPosition();
                keepUpdatingRippleWrapper();
            }
            else {
                isUpdatingRippleWrapper = false;
            }
        });
    };
    createEffect(on(() => ripples[0], () => {
        if (isUpdatingRippleWrapper === false) {
            keepUpdatingRippleWrapper();
        }
    }));
    const rippledElement = createMemo(() => {
        if (canUseDocument()) {
            const firstElementChild = childrenList().find((c) => c instanceof HTMLElement);
            if (typeof firstElementChild !== 'undefined') {
                const rippledElement = firstElementChild;
                rippledElement.addEventListener('click', addNewRipple);
                return rippledElement;
            }
            else {
                console.warn('At least one of the children of Ripple should be an actual HTML element so that the ripple can be positioned based on it!');
            }
        }
    });
    const borderRadius = createMemo(on(rippledElementBoundingBox, () => {
        const el = rippledElement();
        if (el) {
            return getComputedStyle(el).borderRadius;
        }
    }));
    onCleanup(() => {
        const el = rippledElement();
        if (el) {
            el.removeEventListener('click', addNewRipple);
        }
    });
    return (<>
        <Show when={rippledElement()}>
          <Portal>
            <div {...props.wrapperProps} class={mergeClass('absolute cursor-none pointer-events-none', props.wrapperProps?.class)} style={{
            top: (rippledElementBoundingBox()?.top ?? 0) + 'px',
            left: (rippledElementBoundingBox()?.left ?? 0) + 'px',
            width: (rippledElementBoundingBox()?.width ?? 0) + 'px',
            height: (rippledElementBoundingBox()?.height ?? 0) + 'px',
            'border-radius': borderRadius(),
            ...props.wrapperProps?.style
        }}>
              <div {...elProps} class={mergeClass('relative overflow-hidden w-full h-full rounded-[inherit] p-0 m-0', elProps.class)} ref={setRippleContainer}>
                <For each={ripples}>
                  {(ripple) => (<Show when={ripple}>
                      <span class="absolute animate-[ripple_0.6s_linear] rounded-full z-[999] scale-0 opacity-10 bg-[var(--bg)]" style={{
                width: `${ripple.diameter}px`,
                height: `${ripple.diameter}px`,
                left: `${ripple.left}px`,
                top: `${ripple.top}px`,
                '--bg': props.contrastWithBg ?? false
                    ? `var(--${color()}-fg)`
                    : `var(--${color()}-bg)`
            }}/>
                    </Show>)}
                </For>
              </div>
            </div>
          </Portal>
        </Show>

        {childrenList()}
      </>);
});



const Button = componentBuilder()
    .factory(addAccentColoring())
    .factory(extendPropsFrom([
    'color',
    'size',
    'disabled',
    'rippleProps',
    'style',
    'unstyled',
    'children'
]))
    .create((props, color, elProps) => {
    const size = createMemo(() => props.size ?? 'medium');
    return (<Ripple noRipple={props.disabled} contrastWithBg color={color()} {...props.rippleProps}>
        <button type="button" {...elProps} class={mergeClass('inline-flex gap-1 text-center items-center justify-center border-none h-min w-fit box-border shadow-none select-none align-middle ease-in transition-colors !duration-150', !props.disabled &&
            !props.unstyled &&
            'rounded-md outline-none bg-[var(--bg)] text-[var(--fg)] hover:bg-[var(--hover)]', props.disabled && '!bg-[var(--muted-bg)] opacity-30', size() === 'small' && 'px-2 py-1 text-sm', size() === 'medium' && 'px-3 py-2 text-base', size() === 'large' && 'px-4 py-3 text-lg', elProps.class)} style={{
            '--bg': `var(--${color()}-bg)`,
            '--fg': `var(--${color()}-fg)`,
            '--hover': `var(--${color()}-hover)`,
            '--hover-10': `var(--${color()}-hover-10)`,
            ...props.style
        }} aria-disabled={props.disabled}>
          {props.children}
        </button>
      </Ripple>);
});



const TextButton = componentBuilder()
    .factory(extendPropsFrom(['active']))
    .create((props, dftProps) => {
    return (<Button {...dftProps} unstyled rippleProps={{
            contrastWithBg: props.active ?? false,
            ...dftProps.rippleProps
        }} class={mergeClass(!dftProps.disabled &&
            !dftProps.unstyled && [
            'rounded-md outline-none',
            props.active
                ? 'bg-[var(--bg)] text-[var(--fg)]'
                : 'bg-transparent text-[var(--bg)] hover:bg-[var(--hover-10)]'
        ], dftProps.class)}>
        {dftProps.children}
      </Button>);
});



const OutlinedButton = componentBuilder()
    .factory(extendPropsFrom(['active']))
    .create((props, dftProps) => {
    return (<Button {...dftProps} unstyled class={mergeClass(!dftProps.disabled &&
            !dftProps.unstyled && [
            'rounded-md outline outline-2 outline-offset-[-2px] outline-[var(--bg)] hover:bg-[var(--bg)] hover:text-[var(--fg)]',
            props.active
                ? 'bg-[var(--bg)] text-[var(--fg)]'
                : 'bg-transparent text-[var(--bg)]'
        ], dftProps.class)}>
        {dftProps.children}
      </Button>);
});



const IconButton = componentBuilder()
    .factory(extendPropsFrom([
    'squarish',
    'children'
]))
    .create((props, defaultProps) => (<TextButton {...defaultProps} rippleProps={{
        center: true,
        contrastWithBg: true,
        ...defaultProps.rippleProps
    }} unstyled class={mergeClass('!p-0', !defaultProps.unstyled && [
        'aspect-square outline-none',
        (!defaultProps.active || defaultProps.disabled) &&
            'bg-transparent text-[var(--bg)]',
        !defaultProps.disabled && [
            'hover:bg-[var(--bg)] hover:text-[var(--fg)]',
            defaultProps.active && 'bg-[var(--bg)] text-[var(--fg)]'
        ],
        props.squarish ? 'rounded-lg' : 'rounded-full'
    ], defaultProps.size === 'small' && 'min-w-[2rem] min-h-[2rem]', (defaultProps.size === 'medium' ||
        typeof defaultProps.size === 'undefined') &&
        'min-w-[3rem] min-h-[3rem]', defaultProps.size === 'large' && 'min-w-[3.5rem] min-h-[3.5rem]', defaultProps.class)}>
      {props.children}
    </TextButton>));



const List = componentBuilder()
    .factory(extendPropsFrom(['children']))
    .create((props, elProps) => (<ul {...elProps} class={mergeClass('list-none flex flex-col h-fit w-full gap-2 p-0 m-0', elProps.class)}>
      {props.children}
    </ul>));



const ListItem = componentBuilder()
    .factory(addAccentColoring())
    .factory(extendPropsFrom([
    'children',
    'disabled',
    'clickable',
    'active',
    'style',
    'color'
]))
    .create((props, color, elProps) => {
    const clickable = createMemo(() => props.clickable ?? false);
    const disabled = createMemo(() => props.disabled ?? false);
    const active = createMemo(() => props.active ?? false);
    return (<Switch>
        <Match when={clickable()}>
          <Ripple color={color()} contrastWithBg={active()} noRipple={disabled()}>
            <li aria-disabled={disabled()} {...elProps} class={mergeClass('w-full h-fit px-2.5 py-1 rounded-md transition-colors select-none cursor-pointer', disabled()
            ? 'bg-[var(--muted-bg)] text-[var(--muted-fg)] opacity-30'
            : [
                active()
                    ? 'bg-[var(--bg)] text-[var(--fg)]'
                    : 'hover:bg-[var(--hover-10)]'
            ], elProps.class)} style={{
            '--bg': `var(--${color()}-bg)`,
            '--fg': `var(--${color()}-fg)`,
            '--hover-10': `var(--${color()}-hover-10)`,
            ...props.style
        }}>
              {props.children}
            </li>
          </Ripple>
        </Match>

        <Match when={!clickable()}>
          <li {...elProps} class={mergeClass('w-full h-fit px-2.5 py-1', elProps.class)}>
            {props.children}
          </li>
        </Match>
      </Switch>);
});



const ListItemWithDetails = componentBuilder()
    .factory(addAccentColoring())
    .factory(extendPropsFrom([
    'children',
    'color',
    'details',
    'active',
    'showingDetails',
    'disabled'
]))
    .create((props, color, elProps) => {
    const [isDetailsOpen, setDetailsOpen] = createSignal(false);
    createEffect(() => {
        if (typeof props.showingDetails !== 'undefined') {
            setDetailsOpen(props.showingDetails);
        }
    });
    return (<>
        <ListItem {...elProps} class={mergeClass('relative', elProps.class)} color={color()} active={props.active} style={props.style} clickable onClick={mergeCallbacks(elProps.onClick, () => typeof props.showingDetails === 'undefined' &&
            setDetailsOpen((isOpen) => !isOpen))} disabled={props.disabled}>
          {props.children}

          <span class={mergeClass('absolute right-2.5 top-1/2 translate-y-[-50%] origin-center transition-transform', isDetailsOpen() && 'rotate-180')}>
            <KeyboardArrowDown />
          </span>
        </ListItem>

        <Collapse>
          <Show when={isDetailsOpen()}>{props.details}</Show>
        </Collapse>
      </>);
});



/**
 * @description A component used for having a kind of box.
 * It uses the `floating.bg` and `floating.fg` for its *background* and *text* color respectively.
 */
const Box = componentBuilder()
    .factory(extendPropsFrom(['children']))
    .create((props, elProps) => (<div {...elProps} class={mergeClass('px-4 py-5 rounded-lg box-border w-full border border-solid bg-[var(--floating-bg)] border-[var(--floating-border)] text-[var(--floating-fg)]', elProps.class)} style={elProps.style}>
      {props.children}
    </div>));



const Dropdown = componentBuilder()
    .factory(extendPropsFrom([
    'for',
    'visible',
    'offsetFromAnchor',
    'style',
    'children'
]))
    .create((props, elProps) => {
    const offset = createMemo(() => props.offsetFromAnchor ?? 10);
    const boundingRect = createMemo(on(() => [props.for, props.visible], () => getAbsoluteBoundingRect(props.for)));
    return (<Show when={props.visible}>
        <div {...elProps} class={mergeClass('absolute z-10 h-fit overflow-y-auto overflow-x-hidden rounded-xl box-border p-2', 'bg-[var(--floating-bg)] text-[var(--floating-fg)] border border-solid border-[var(--floating-border)]', elProps.class)} style={{
            left: `${boundingRect().left + boundingRect().width}px`,
            top: `${boundingRect().top + boundingRect().height + offset()}px`,
            width: `${boundingRect().width}px`,
            translate: '-100%',
            ...props.style
        }}>
          {props.children}
        </div>
      </Show>);
});



const DialogInternal = componentBuilder()
    .factory(extendPropsFrom([
    'title',
    'extraElementsInFooter',
    'visible',
    'onOk',
    'onHidden',
    'onCancel',
    'children'
]))
    .create((props, elProps) => (<Fade onAfterExit={() => props.onHidden && props.onHidden()}>
      <Show when={props.visible}>
        <div class={mergeClass('absolute left-0 top-0 w-screen h-screen z-20 bg-black/40 backdrop-blur', !props.visible && 'pointer-events-none')} classList={{
        visible: props.visible
    }} onClick={(event) => props.onCancel && props.visible
        ? props.onCancel(event)
        : undefined}>
          <Box {...elProps} onClick={mergeCallbacks(elProps.onClick, (event) => {
        event.stopPropagation();
    })} class={mergeClass('absolute left-1/2 top-1/2 w-full h-fit max-w-[568px] translate-x-[-50%] translate-y-[-50%]', elProps.class)}>
            <div class="header">{props.title}</div>

            <Divisor />

            <div class="body">{props.children}</div>

            <Divisor />

            <div class="flex flex-row w-full">
              {props.extraElementsInFooter}

              <div class="ml-auto flex gap-4 &>*">
                <OutlinedButton class="h-full" color="danger" onClick={props.onCancel}>
                  Cancel
                </OutlinedButton>
                <Button class="h-full" onClick={props.onOk} color="accent">
                  Ok
                </Button>
              </div>
            </div>
          </Box>
        </div>
      </Show>
    </Fade>));
const Dialog = (props) => {
    return (<Portal>
      <DialogInternal {...props}/>
    </Portal>);
};
function createDialog(options) {
    createRoot((dispose) => {
        const container = document.createElement('div');
        const [isModalVisible, setModalVisible] = createSignal(false);
        const modal = createMemo(() => (<DialogInternal {...options} visible={isModalVisible()} onHidden={() => {
                document.body.removeChild(container);
                dispose();
            }} onOk={(...args) => {
                setModalVisible(false);
                if (options.onOk) {
                    options.onOk(...args);
                }
            }} onCancel={(...args) => {
                setModalVisible(false);
                if (options.onCancel) {
                    options.onCancel(...args);
                }
            }}>
        {options.body}
      </DialogInternal>));
        // eslint-disable-next-line solid/reactivity
        insert(container, modal);
        document.body.appendChild(container);
        setModalVisible(true);
    });
}



export { Box, Button, Dialog, Dropdown, IconButton, List, ListItem, ListItemWithDetails, OutlinedButton, Ripple, TextButton, createDialog, setupTerraprisma, useTheme };
