/**
 * Copyright 2011-2021 Asakusa Framework Team.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.asakusafw.utils.java.parser.javadoc;

import java.util.ArrayList;
import java.util.List;

import com.asakusafw.utils.java.internal.parser.javadoc.ir.IrBasicTypeKind;
import com.asakusafw.utils.java.internal.parser.javadoc.ir.IrDocArrayType;
import com.asakusafw.utils.java.internal.parser.javadoc.ir.IrDocBasicType;
import com.asakusafw.utils.java.internal.parser.javadoc.ir.IrDocBlock;
import com.asakusafw.utils.java.internal.parser.javadoc.ir.IrDocComment;
import com.asakusafw.utils.java.internal.parser.javadoc.ir.IrDocElementVisitor;
import com.asakusafw.utils.java.internal.parser.javadoc.ir.IrDocField;
import com.asakusafw.utils.java.internal.parser.javadoc.ir.IrDocFragment;
import com.asakusafw.utils.java.internal.parser.javadoc.ir.IrDocMethod;
import com.asakusafw.utils.java.internal.parser.javadoc.ir.IrDocMethodParameter;
import com.asakusafw.utils.java.internal.parser.javadoc.ir.IrDocNamedType;
import com.asakusafw.utils.java.internal.parser.javadoc.ir.IrDocQualifiedName;
import com.asakusafw.utils.java.internal.parser.javadoc.ir.IrDocSimpleName;
import com.asakusafw.utils.java.internal.parser.javadoc.ir.IrDocText;
import com.asakusafw.utils.java.model.syntax.BasicTypeKind;
import com.asakusafw.utils.java.model.syntax.DocBlock;
import com.asakusafw.utils.java.model.syntax.DocElement;
import com.asakusafw.utils.java.model.syntax.DocMethodParameter;
import com.asakusafw.utils.java.model.syntax.Javadoc;
import com.asakusafw.utils.java.model.syntax.ModelFactory;
import com.asakusafw.utils.java.model.syntax.Name;
import com.asakusafw.utils.java.model.syntax.NamedType;
import com.asakusafw.utils.java.model.syntax.SimpleName;
import com.asakusafw.utils.java.model.syntax.Type;

/**
 * Converts {@link IrDocComment} models into Java DOM models.
 */
public class JavadocConverter {

    private final ModelFactory factory;

    private final JavadocParser parser;

    /**
     * Creates a new instance.
     * @param factory the Java DOM factory
     * @throws IllegalArgumentException if the parameter is {@code null}
     */
    public JavadocConverter(ModelFactory factory) {
        this.factory = factory;
        JavadocParserBuilder builder = new JavadocParserBuilder();
        builder.addSpecialStandAloneBlockParser(new FollowsNamedTypeBlockParser(
            "exception", //$NON-NLS-1$
            "throws" //$NON-NLS-1$
        ));
        builder.addSpecialStandAloneBlockParser(new FollowsReferenceBlockParser(
            "see" //$NON-NLS-1$
        ));
        builder.addSpecialStandAloneBlockParser(new ParamBlockParser(
            "param" //$NON-NLS-1$
        ));
        builder.addSpecialStandAloneBlockParser(new SerialFieldBlockParser(
            "serialField" //$NON-NLS-1$
        ));

        builder.addSpecialInlineBlockParser(new FollowsReferenceBlockParser(
            "link", //$NON-NLS-1$
            "linkplain" //$NON-NLS-1$
        ));
        this.parser = builder.build();
    }

    /**
     * Analyzes the documentation comment string and returns the corresponded {@link Javadoc} object.
     * @param content the documentation content
     * @param offset the starting character offset of the comment string in the compilation unit
     * @return the analyzed object
     * @throws JavadocParseException if the documentation comment is malformed
     * @throws IllegalArgumentException if the parameter is {@code null}
     */
    public Javadoc convert(String content, int offset) throws JavadocParseException {
        if (content == null) {
            throw new IllegalArgumentException("content must not be null"); //$NON-NLS-1$
        }
        JavadocScanner scanner = DefaultJavadocScanner.newInstance(content);
        IrDocComment ir = parser.parse(scanner);
        return convert(ir, offset);
    }

    private Javadoc convert(IrDocComment comment, int offset) {
        assert comment != null;
        Mapper mapper = new Mapper(factory, offset);

        List<DocBlock> blocks = new ArrayList<>();
        for (IrDocBlock block : comment.getBlocks()) {
            blocks.add((DocBlock) block.accept(mapper, null));
        }
        return factory.newJavadoc(blocks);
    }

    private static class Mapper
            extends IrDocElementVisitor<DocElement, Void> {

        final ModelFactory factory;

        private final TypeMapper types;

        Mapper(ModelFactory factory, int offset) {
            assert factory != null;
            this.factory = factory;
            this.types = new TypeMapper();
        }

        @Override
        public DocElement visitBlock(IrDocBlock elem, Void context) {
            String tag = elem.getTag();
            List<DocElement> elements = new ArrayList<>();
            for (IrDocFragment f : elem.getFragments()) {
                elements.add(f.accept(this, null));
            }
            return factory.newDocBlock(
                tag == null ? "" : tag, //$NON-NLS-1$
                elements);
        }

        @Override
        public DocElement visitText(IrDocText elem, Void context) {
            return factory.newDocText(elem.getContent());
        }

        @Override
        public DocElement visitSimpleName(IrDocSimpleName elem, Void context) {
            return factory.newSimpleName(elem.getIdentifier());
        }

        @Override
        public DocElement visitQualifiedName(IrDocQualifiedName elem, Void context) {
            Name qualifier = (Name) elem.getQualifier().accept(this, null);
            SimpleName simple = (SimpleName) elem.getName().accept(this, null);
            return factory.newQualifiedName(qualifier, simple);
        }

        @Override
        public DocElement visitField(IrDocField elem, Void context) {
            Type type = declaring(elem.getDeclaringType());
            SimpleName name = (SimpleName) elem.getName().accept(this, null);
            return factory.newDocField(type, name);
        }

        @Override
        public DocElement visitMethod(IrDocMethod elem, Void context) {
            Type type = declaring(elem.getDeclaringType());
            SimpleName name = (SimpleName) elem.getName().accept(this, null);
            List<DocMethodParameter> params = new ArrayList<>();
            for (IrDocMethodParameter p : elem.getParameters()) {
                params.add(convert(p));
            }
            return factory.newDocMethod(type, name, params);
        }

        private DocMethodParameter convert(IrDocMethodParameter elem) {
            Type type = elem.getType().accept(types, this);
            SimpleName name;
            if (elem.getName() != null) {
                name = (SimpleName) elem.getName().accept(this, null);
            } else {
                name = null;
            }
            return factory.newDocMethodParameter(
                type,
                name,
                elem.isVariableArity());
        }

        @Override
        public DocElement visitNamedType(IrDocNamedType elem, Void context) {
            Name name = (Name) elem.getName().accept(this, null);
            return factory.newNamedType(name);
        }

        private NamedType declaring(IrDocNamedType declaringType) {
            if (declaringType == null) {
                return null;
            }
            return (NamedType) visitNamedType(declaringType, null);
        }
    }

    private static class TypeMapper extends IrDocElementVisitor<Type, Mapper> {

        TypeMapper() {
            return;
        }

        @Override
        public Type visitArrayType(IrDocArrayType elem, Mapper context) {
            Type component = elem.getComponentType().accept(this, context);
            return context.factory.newArrayType(component);
        }

        @Override
        public Type visitBasicType(IrDocBasicType elem, Mapper context) {
            BasicTypeKind kind = convert(elem.getTypeKind());
            return context.factory.newBasicType(kind);
        }

        @Override
        public Type visitNamedType(IrDocNamedType elem, Mapper context) {
            Name name = (Name) elem.getName().accept(context, null);
            return context.factory.newNamedType(name);
        }

        private static BasicTypeKind convert(IrBasicTypeKind kind) {
            switch (kind) {
            case BOOLEAN:
                return BasicTypeKind.BOOLEAN;
            case BYTE:
                return BasicTypeKind.BYTE;
            case CHAR:
                return BasicTypeKind.CHAR;
            case DOUBLE:
                return BasicTypeKind.DOUBLE;
            case FLOAT:
                return BasicTypeKind.FLOAT;
            case INT:
                return BasicTypeKind.INT;
            case LONG:
                return BasicTypeKind.LONG;
            case SHORT:
                return BasicTypeKind.SHORT;
            case VOID:
                return BasicTypeKind.VOID;
            default:
                throw new AssertionError(kind);
            }
        }
    }
}
