import cloneDeep from 'lodash.clonedeep';
import { isAbsolute } from 'path';
import postcss from 'postcss';
import { Diagnostics } from './diagnostics';
import {
    DeclStylableProps,
    Imported,
    SDecl,
    SRule,
    StylableMeta,
    StylableSymbol,
} from './stylable-processor';

import {
    fixChunkOrdering,
    isNodeMatch,
    parseSelector,
    SelectorAstNode,
    stringifySelector,
    traverseNode,
} from './selector-utils';
import { ImportSymbol } from './stylable-meta';
import { valueMapping } from './stylable-value-parsers';
const replaceRuleSelector = require('postcss-selector-matches/dist/replaceRuleSelector');

export const CUSTOM_SELECTOR_RE = /:--[\w-]+/g;

export function isValidDeclaration(decl: postcss.Declaration) {
    return typeof decl.value === 'string';
}

export function expandCustomSelectors(
    rule: postcss.Rule,
    customSelectors: Record<string, string>,
    diagnostics?: Diagnostics
): string {
    if (rule.selector.includes(':--')) {
        rule.selector = rule.selector.replace(
            CUSTOM_SELECTOR_RE,
            (extensionName, _matches, selector) => {
                if (!customSelectors[extensionName] && diagnostics) {
                    diagnostics.warn(rule, `The selector '${rule.selector}' is undefined`, {
                        word: rule.selector,
                    });
                    return selector;
                }
                // TODO: support nested CustomSelectors
                return ':matches(' + customSelectors[extensionName] + ')';
            }
        );

        return (rule.selector = transformMatchesOnRule(rule, false) as string);
    }
    return rule.selector;
}

export function transformMatchesOnRule(rule: postcss.Rule, lineBreak: boolean) {
    return replaceRuleSelector(rule, { lineBreak });
}

export function scopeSelector(
    scopeSelectorRule: string,
    targetSelectorRule: string,
    rootScopeLevel = false
): { selector: string; selectorAst: SelectorAstNode } {
    const scopingSelectorAst = parseSelector(scopeSelectorRule);
    const targetSelectorAst = parseSelector(targetSelectorRule);

    const nodes: any[] = [];
    targetSelectorAst.nodes.forEach((targetSelector) => {
        scopingSelectorAst.nodes.forEach((scopingSelector) => {
            const outputSelector: any = cloneDeep(targetSelector);

            outputSelector.before = scopingSelector.before || outputSelector.before;

            const first = outputSelector.nodes[0];
            const parentRef = first.type === 'invalid' && first.value === '&';
            const globalSelector = first.type === 'nested-pseudo-class' && first.name === 'global';

            const startsWithScoping = rootScopeLevel
                ? scopingSelector.nodes.every((node: any, i) => {
                      const o = outputSelector.nodes[i];
                      for (const k in node) {
                          if (node[k] !== o[k]) {
                              return false;
                          }
                      }
                      return true;
                  })
                : false;

            if (
                first &&
                first.type !== 'spacing' &&
                !parentRef &&
                !startsWithScoping &&
                !globalSelector
            ) {
                outputSelector.nodes.unshift(...cloneDeep(scopingSelector.nodes), {
                    type: 'spacing',
                    value: ' ',
                });
            }

            traverseNode(outputSelector, (node, i, nodes) => {
                if (node.type === 'invalid' && node.value === '&') {
                    nodes.splice(i, 1, ...cloneDeep(scopingSelector.nodes));
                }
            });

            nodes.push(outputSelector);
        });
    });

    scopingSelectorAst.nodes = nodes;

    return {
        selector: stringifySelector(scopingSelectorAst),
        selectorAst: scopingSelectorAst,
    };
}

export function mergeRules(mixinAst: postcss.Root, rule: postcss.Rule) {
    let mixinRoot: postcss.Rule | null = null;
    mixinAst.walkRules((mixinRule: postcss.Rule) => {
        if (mixinRule.selector === '&' && !mixinRoot) {
            mixinRoot = mixinRule;
        } else {
            const parentRule = mixinRule.parent;
            if (parentRule.type === 'atrule' && parentRule.name === 'keyframes') {
                return;
            }
            const out = scopeSelector(rule.selector, mixinRule.selector);
            mixinRule.selector = out.selector;
            // mixinRule.selectorAst = out.selectorAst;
        }
    });

    if (mixinAst.nodes) {
        let nextRule: postcss.Rule | postcss.AtRule = rule;
        let mixinEntry: postcss.Declaration | null = null;

        rule.walkDecls(valueMapping.mixin, (decl) => {
            mixinEntry = decl;
        });
        if (!mixinEntry) {
            throw rule.error('missing mixin entry');
        }
        // TODO: handle rules before and after decl on entry
        mixinAst.nodes.slice().forEach((node) => {
            if (node === mixinRoot) {
                node.walkDecls((node) => {
                    rule.insertBefore(mixinEntry!, node);
                });
            } else if (node.type === 'decl') {
                rule.insertBefore(mixinEntry!, node);
            } else if (node.type === 'rule' || node.type === 'atrule') {
                if (rule.parent.last === nextRule) {
                    rule.parent.append(node);
                } else {
                    rule.parent.insertAfter(nextRule, node);
                }
                nextRule = node;
            }
        });
    }

    return rule;
}

export function createSubsetAst<T extends postcss.Root | postcss.AtRule>(
    root: postcss.Root | postcss.AtRule,
    selectorPrefix: string,
    mixinTarget?: T,
    isRoot = false
): T {
    // keyframes on class mixin?

    const prefixType = parseSelector(selectorPrefix).nodes[0].nodes[0];
    const containsPrefix = containsMatchInFirstChunk.bind(null, prefixType);
    const mixinRoot = mixinTarget ? mixinTarget : postcss.root();

    root.nodes!.forEach((node) => {
        if (node.type === 'rule') {
            const ast = isRoot
                ? scopeSelector(selectorPrefix, node.selector, true).selectorAst
                : parseSelector(node.selector);

            const matchesSelectors = isRoot
                ? ast.nodes
                : ast.nodes.filter((node) => containsPrefix(node));

            if (matchesSelectors.length) {
                const selector = stringifySelector({
                    ...ast,
                    nodes: matchesSelectors.map((selectorNode) => {
                        if (!isRoot) {
                            fixChunkOrdering(selectorNode, prefixType);
                        }

                        return destructiveReplaceNode(selectorNode, prefixType, {
                            type: 'invalid',
                            value: '&',
                        } as SelectorAstNode);
                    }),
                });

                mixinRoot.append(node.clone({ selector }));
            }
        } else if (node.type === 'atrule') {
            if (node.name === 'media') {
                const mediaSubset = createSubsetAst(
                    node,
                    selectorPrefix,
                    postcss.atRule({
                        params: node.params,
                        name: node.name,
                    }),
                    isRoot
                );
                if (mediaSubset.nodes) {
                    mixinRoot.append(mediaSubset);
                }
            } else if (isRoot) {
                mixinRoot.append(node.clone());
            }
        } else {
            // TODO: add warn?
        }
    });

    return mixinRoot as T;
}

export function removeUnusedRules(
    ast: postcss.Root,
    meta: StylableMeta,
    _import: Imported,
    usedFiles: string[],
    resolvePath: (ctx: string, path: string) => string
): void {
    const isUnusedImport = !usedFiles.includes(_import.from);

    if (isUnusedImport) {
        const symbols = Object.keys(_import.named).concat(_import.defaultExport); // .filter(Boolean);
        ast.walkRules((rule) => {
            let shouldOutput = true;
            traverseNode((rule as SRule).selectorAst, (node) => {
                // TODO: remove.
                if (symbols.includes(node.name)) {
                    return (shouldOutput = false);
                }
                const symbol = meta.mappedSymbols[node.name];
                if (symbol && (symbol._kind === 'class' || symbol._kind === 'element')) {
                    let extend = symbol[valueMapping.extends] || symbol.alias;
                    extend = extend && extend._kind !== 'import' ? extend.alias || extend : extend;

                    if (
                        extend &&
                        extend._kind === 'import' &&
                        !usedFiles.includes(resolvePath(meta.source, extend.import.from))
                    ) {
                        return (shouldOutput = false);
                    }
                }
                return undefined;
            });
            // TODO: optimize the multiple selectors
            if (!shouldOutput && (rule as SRule).selectorAst.nodes.length <= 1) {
                rule.remove();
            }
        });
    }
}

export function findDeclaration(importNode: Imported, test: any) {
    const fromIndex = importNode.rule.nodes!.findIndex(test);
    return importNode.rule.nodes![fromIndex] as postcss.Declaration;
}

// TODO: What is this?
export function findRule(
    root: postcss.Root,
    selector: string,
    test: any = (statement: any) => statement.prop === valueMapping.extends
): null | postcss.Declaration {
    let found: any = null;
    root.walkRules(selector, (rule) => {
        const declarationIndex = rule.nodes ? rule.nodes.findIndex(test) : -1;
        if ((rule as SRule).isSimpleSelector && !!~declarationIndex) {
            found = rule.nodes![declarationIndex];
        }
    });
    return found;
}

export function getDeclStylable(decl: SDecl): DeclStylableProps {
    if (decl.stylable) {
        return decl.stylable;
    } else {
        decl.stylable = decl.stylable ? decl.stylable : { sourceValue: '' };
        return decl.stylable;
    }
}

function destructiveReplaceNode(
    ast: SelectorAstNode,
    matchNode: SelectorAstNode,
    replacementNode: SelectorAstNode
) {
    traverseNode(ast, (node) => {
        if (isNodeMatch(node, matchNode)) {
            node.type = 'selector';
            node.nodes = [replacementNode];
        }
    });
    return ast;
}

function containsMatchInFirstChunk(prefixType: SelectorAstNode, selectorNode: SelectorAstNode) {
    let isMatch = false;
    traverseNode(selectorNode, (node) => {
        if (node.type === 'operator' || node.type === 'spacing') {
            return false;
        } else if (node.type === 'nested-pseudo-class') {
            return true;
        } else if (isNodeMatch(node, prefixType)) {
            isMatch = true;
            return false;
        }
        return undefined;
    });
    return isMatch;
}

export function getSourcePath(root: postcss.Root, diagnostics: Diagnostics) {
    const source = (root.source && root.source.input.file) || '';
    if (!source) {
        diagnostics.error(root, 'missing source filename');
    } else if (!isAbsolute(source)) {
        throw new Error('source filename is not absolute path: "' + source + '"');
    }
    return source;
}

export function getAlias(symbol: StylableSymbol): ImportSymbol | undefined {
    if (symbol._kind === 'class' || symbol._kind === 'element') {
        if (!symbol[valueMapping.extends]) {
            return symbol.alias;
        }
    }

    return undefined;
}

export function generateScopedCSSVar(namespace: string, varName: string) {
    return `--${namespace}-${varName}`;
}

export function isCSSVarProp(value: string) {
    return value.startsWith('--');
}

export function isValidClassName(className: string) {
    const test = /^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$/g; // checks valid classname
    return !!className.match(test);
}
