import { errorUtils } from '@0xproject/utils';
import * as _ from 'lodash';

import { DocsInfo } from '../docs_info';
import {
    CustomType,
    CustomTypeChild,
    DocAgnosticFormat,
    DocSection,
    IndexSignature,
    KindString,
    Parameter,
    Property,
    SectionsMap,
    Type,
    TypeDocNode,
    TypeDocType,
    TypeDocTypes,
    TypeParameter,
    TypescriptFunction,
    TypescriptMethod,
} from '../types';

export const typeDocUtils = {
    isType(entity: TypeDocNode): boolean {
        return (
            entity.kindString === KindString.Interface ||
            entity.kindString === KindString.Function ||
            entity.kindString === KindString.TypeAlias ||
            entity.kindString === KindString.Variable ||
            entity.kindString === KindString.Enumeration
        );
    },
    isMethod(entity: TypeDocNode): boolean {
        return entity.kindString === KindString.Method;
    },
    isConstructor(entity: TypeDocNode): boolean {
        return entity.kindString === KindString.Constructor;
    },
    isProperty(entity: TypeDocNode): boolean {
        return entity.kindString === KindString.Property;
    },
    isPrivateOrProtectedProperty(propertyName: string): boolean {
        return _.startsWith(propertyName, '_');
    },
    getModuleDefinitionsBySectionName(versionDocObj: TypeDocNode, configModulePaths: string[]): TypeDocNode[] {
        const moduleDefinitions: TypeDocNode[] = [];
        const jsonModules = versionDocObj.children;
        _.each(jsonModules, jsonMod => {
            _.each(configModulePaths, configModulePath => {
                if (_.includes(configModulePath, jsonMod.name)) {
                    moduleDefinitions.push(jsonMod);
                }
            });
        });
        return moduleDefinitions;
    },
    convertToDocAgnosticFormat(typeDocJson: TypeDocNode, docsInfo: DocsInfo): DocAgnosticFormat {
        const subMenus = _.values(docsInfo.getMenu());
        const orderedSectionNames = _.flatten(subMenus);
        const docAgnosticFormat: DocAgnosticFormat = {};
        _.each(orderedSectionNames, sectionName => {
            const modulePathsIfExists = docsInfo.getModulePathsIfExists(sectionName);
            if (_.isUndefined(modulePathsIfExists)) {
                return; // no-op
            }
            const packageDefinitions = typeDocUtils.getModuleDefinitionsBySectionName(typeDocJson, modulePathsIfExists);
            let packageDefinitionWithMergedChildren;
            if (_.isEmpty(packageDefinitions)) {
                return; // no-op
            } else if (packageDefinitions.length === 1) {
                packageDefinitionWithMergedChildren = packageDefinitions[0];
            } else {
                // HACK: For now, if there are two modules to display in a single section,
                // we simply concat the children. This works for our limited use-case where
                // we want to display types stored in two files under a single section
                packageDefinitionWithMergedChildren = packageDefinitions[0];
                for (let i = 1; i < packageDefinitions.length; i++) {
                    packageDefinitionWithMergedChildren.children = [
                        ...packageDefinitionWithMergedChildren.children,
                        ...packageDefinitions[i].children,
                    ];
                }
            }

            let entities;
            let packageComment = '';
            // HACK: We assume 1 exported class per file
            const classChildren = _.filter(packageDefinitionWithMergedChildren.children, (child: TypeDocNode) => {
                return child.kindString === KindString.Class;
            });
            if (classChildren.length > 1 && sectionName !== 'types') {
                throw new Error('`react-docs` only supports projects with 1 exported class per file');
            }
            const isClassExport = packageDefinitionWithMergedChildren.children[0].kindString === KindString.Class;
            const isObjectLiteralExport =
                packageDefinitionWithMergedChildren.children[0].kindString === KindString.ObjectLiteral;
            if (isClassExport) {
                entities = packageDefinitionWithMergedChildren.children[0].children;
                const commentObj = packageDefinitionWithMergedChildren.children[0].comment;
                packageComment = !_.isUndefined(commentObj) ? commentObj.shortText : packageComment;
            } else if (isObjectLiteralExport) {
                entities = packageDefinitionWithMergedChildren.children[0].children;
                const commentObj = packageDefinitionWithMergedChildren.children[0].comment;
                packageComment = !_.isUndefined(commentObj) ? commentObj.shortText : packageComment;
            } else {
                entities = packageDefinitionWithMergedChildren.children;
            }

            const docSection = typeDocUtils._convertEntitiesToDocSection(entities, docsInfo, sectionName);
            docSection.comment = packageComment;
            docAgnosticFormat[sectionName] = docSection;
        });
        return docAgnosticFormat;
    },
    _convertEntitiesToDocSection(entities: TypeDocNode[], docsInfo: DocsInfo, sectionName: string): DocSection {
        const docSection: DocSection = {
            comment: '',
            constructors: [],
            methods: [],
            functions: [],
            properties: [],
            types: [],
        };

        let isConstructor;
        _.each(entities, entity => {
            switch (entity.kindString) {
                case KindString.Constructor:
                    isConstructor = true;
                    const constructor = typeDocUtils._convertMethod(
                        entity,
                        isConstructor,
                        docsInfo.sections,
                        sectionName,
                        docsInfo.id,
                    );
                    docSection.constructors.push(constructor);
                    break;

                case KindString.Function:
                    if (entity.flags.isExported) {
                        const func = typeDocUtils._convertFunction(entity, docsInfo.sections, sectionName, docsInfo.id);
                        docSection.functions.push(func);
                    }
                    break;

                case KindString.Method:
                    if (entity.flags.isPublic) {
                        isConstructor = false;
                        const method = typeDocUtils._convertMethod(
                            entity,
                            isConstructor,
                            docsInfo.sections,
                            sectionName,
                            docsInfo.id,
                        );
                        docSection.methods.push(method);
                    }
                    break;

                case KindString.Property:
                    if (!typeDocUtils.isPrivateOrProtectedProperty(entity.name)) {
                        const property = typeDocUtils._convertProperty(
                            entity,
                            docsInfo.sections,
                            sectionName,
                            docsInfo.id,
                        );
                        docSection.properties.push(property);
                    }
                    break;

                case KindString.Interface:
                case KindString.Variable:
                case KindString.Enumeration:
                case KindString.TypeAlias:
                    if (docsInfo.isPublicType(entity.name)) {
                        const customType = typeDocUtils._convertCustomType(
                            entity,
                            docsInfo.sections,
                            sectionName,
                            docsInfo.id,
                        );
                        const seenTypeNames = _.map(docSection.types, t => t.name);
                        const isUnseen = !_.includes(seenTypeNames, customType.name);
                        if (isUnseen) {
                            docSection.types.push(customType);
                        }
                    }
                    break;

                case KindString.Class:
                    // We currently do not support more then a single class per file
                    // except for the types section, where we ignore classes since we
                    // only want to render type definitions.
                    break;

                default:
                    throw errorUtils.spawnSwitchErr('kindString', entity.kindString);
            }
        });
        return docSection;
    },
    _convertCustomType(entity: TypeDocNode, sections: SectionsMap, sectionName: string, docId: string): CustomType {
        const typeIfExists = !_.isUndefined(entity.type)
            ? typeDocUtils._convertType(entity.type, sections, sectionName, docId)
            : undefined;
        const isConstructor = false;
        const methodIfExists = !_.isUndefined(entity.declaration)
            ? typeDocUtils._convertMethod(entity.declaration, isConstructor, sections, sectionName, docId)
            : undefined;
        const doesIndexSignatureExist = !_.isUndefined(entity.indexSignature);
        const isIndexSignatureArray = _.isArray(entity.indexSignature);
        // HACK: TypeDoc Versions <0.9.0 indexSignature is of type TypeDocNode[]
        // Versions >0.9.0 have it as type TypeDocNode
        const indexSignature =
            doesIndexSignatureExist && isIndexSignatureArray
                ? (entity.indexSignature as TypeDocNode[])[0]
                : (entity.indexSignature as TypeDocNode);
        const indexSignatureIfExists = doesIndexSignatureExist
            ? typeDocUtils._convertIndexSignature(indexSignature, sections, sectionName, docId)
            : undefined;
        const commentIfExists =
            !_.isUndefined(entity.comment) && !_.isUndefined(entity.comment.shortText)
                ? entity.comment.shortText
                : undefined;

        const childrenIfExist = !_.isUndefined(entity.children)
            ? _.map(entity.children, (child: TypeDocNode) => {
                  let childTypeIfExists = !_.isUndefined(child.type)
                      ? typeDocUtils._convertType(child.type, sections, sectionName, docId)
                      : undefined;
                  if (child.kindString === KindString.Method) {
                      childTypeIfExists = {
                          name: child.name,
                          typeDocType: TypeDocTypes.Reflection,
                          method: typeDocUtils._convertMethod(child, isConstructor, sections, sectionName, docId),
                      };
                  }
                  const c: CustomTypeChild = {
                      name: child.name,
                      type: childTypeIfExists,
                      defaultValue: child.defaultValue,
                  };
                  return c;
              })
            : undefined;

        const customType = {
            name: entity.name,
            kindString: entity.kindString,
            type: typeIfExists,
            method: methodIfExists,
            indexSignature: indexSignatureIfExists,
            defaultValue: entity.defaultValue,
            comment: commentIfExists,
            children: childrenIfExist,
        };
        return customType;
    },
    _convertIndexSignature(
        entity: TypeDocNode,
        sections: SectionsMap,
        sectionName: string,
        docId: string,
    ): IndexSignature {
        const key = entity.parameters[0];
        const indexSignature = {
            keyName: key.name,
            keyType: typeDocUtils._convertType(key.type, sections, sectionName, docId),
            valueName: entity.type.name,
        };
        return indexSignature;
    },
    _convertProperty(entity: TypeDocNode, sections: SectionsMap, sectionName: string, docId: string): Property {
        const source = entity.sources[0];
        const commentIfExists = !_.isUndefined(entity.comment) ? entity.comment.shortText : undefined;
        const property = {
            name: entity.name,
            type: typeDocUtils._convertType(entity.type, sections, sectionName, docId),
            source: {
                fileName: source.fileName,
                line: source.line,
            },
            comment: commentIfExists,
        };
        return property;
    },
    _convertMethod(
        entity: TypeDocNode,
        isConstructor: boolean,
        sections: SectionsMap,
        sectionName: string,
        docId: string,
    ): TypescriptMethod {
        const signature = entity.signatures[0];
        const source = entity.sources[0];
        const hasComment = !_.isUndefined(signature.comment);
        const isStatic = _.isUndefined(entity.flags.isStatic) ? false : entity.flags.isStatic;

        // HACK: we use the fact that the sectionName is the same as the property name at the top-level
        // of the public interface. In the future, we shouldn't use this hack but rather get it from the JSON.
        let callPath;
        if (isConstructor || entity.name === '__type') {
            callPath = '';
            // TODO: Get rid of this 0x-specific logic
        } else if (docId === 'ZERO_EX_JS') {
            const topLevelInterface = isStatic ? 'ZeroEx.' : 'zeroEx.';
            callPath =
                !_.isUndefined(sections.zeroEx) && sectionName !== sections.zeroEx
                    ? `${topLevelInterface}${sectionName}.`
                    : topLevelInterface;
        } else {
            callPath = `${sectionName}.`;
        }

        const parameters = _.map(signature.parameters, param => {
            return typeDocUtils._convertParameter(param, sections, sectionName, docId);
        });
        const returnType = typeDocUtils._convertType(signature.type, sections, sectionName, docId);
        const typeParameter = _.isUndefined(signature.typeParameter)
            ? undefined
            : typeDocUtils._convertTypeParameter(signature.typeParameter[0], sections, sectionName, docId);

        const method = {
            isConstructor,
            isStatic,
            name: signature.name,
            comment: hasComment ? signature.comment.shortText : undefined,
            returnComment: hasComment && signature.comment.returns ? signature.comment.returns : undefined,
            source: {
                fileName: source.fileName,
                line: source.line,
            },
            callPath,
            parameters,
            returnType,
            typeParameter,
        };
        return method;
    },
    _convertFunction(
        entity: TypeDocNode,
        sections: SectionsMap,
        sectionName: string,
        docId: string,
    ): TypescriptFunction {
        const signature = entity.signatures[0];
        const source = entity.sources[0];
        const hasComment = !_.isUndefined(signature.comment);

        const parameters = _.map(signature.parameters, param => {
            return typeDocUtils._convertParameter(param, sections, sectionName, docId);
        });
        const returnType = typeDocUtils._convertType(signature.type, sections, sectionName, docId);
        const typeParameter = _.isUndefined(signature.typeParameter)
            ? undefined
            : typeDocUtils._convertTypeParameter(signature.typeParameter[0], sections, sectionName, docId);

        const func = {
            name: signature.name,
            comment: hasComment ? signature.comment.shortText : undefined,
            returnComment: hasComment && signature.comment.returns ? signature.comment.returns : undefined,
            source: {
                fileName: source.fileName,
                line: source.line,
            },
            parameters,
            returnType,
            typeParameter,
        };
        return func;
    },
    _convertTypeParameter(
        entity: TypeDocNode,
        sections: SectionsMap,
        sectionName: string,
        docId: string,
    ): TypeParameter {
        const type = typeDocUtils._convertType(entity.type, sections, sectionName, docId);
        const parameter = {
            name: entity.name,
            type,
        };
        return parameter;
    },
    _convertParameter(entity: TypeDocNode, sections: SectionsMap, sectionName: string, docId: string): Parameter {
        let comment = '<No comment>';
        if (entity.comment && entity.comment.shortText) {
            comment = entity.comment.shortText;
        } else if (entity.comment && entity.comment.text) {
            comment = entity.comment.text;
        }

        const isOptional = !_.isUndefined(entity.flags.isOptional) ? entity.flags.isOptional : false;

        const type = typeDocUtils._convertType(entity.type, sections, sectionName, docId);

        const parameter = {
            name: entity.name,
            comment,
            isOptional,
            defaultValue: entity.defaultValue,
            type,
        };
        return parameter;
    },
    _convertType(entity: TypeDocType, sections: SectionsMap, sectionName: string, docId: string): Type {
        const typeArguments = _.map(entity.typeArguments, typeArgument => {
            return typeDocUtils._convertType(typeArgument, sections, sectionName, docId);
        });
        const types = _.map(entity.types, t => {
            return typeDocUtils._convertType(t, sections, sectionName, docId);
        });

        const isConstructor = false;
        const methodIfExists = !_.isUndefined(entity.declaration)
            ? typeDocUtils._convertMethod(entity.declaration, isConstructor, sections, sectionName, docId)
            : undefined;

        const elementTypeIfExists = !_.isUndefined(entity.elementType)
            ? {
                  name: entity.elementType.name,
                  typeDocType: entity.elementType.type,
              }
            : undefined;

        const type = {
            name: entity.name,
            value: entity.value,
            typeDocType: entity.type,
            typeArguments,
            elementType: elementTypeIfExists,
            types,
            method: methodIfExists,
        };
        return type;
    },
};
