/*
 * Copyright (C) 2017 exzogeni.com
 *
 * 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 alchemy.sqlite.compiler;

import alchemy.annotations.Entry;
import com.squareup.javapoet.ClassName;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.SimpleTypeVisitor7;
import java.util.List;
import java.util.regex.Matcher;

class RelationRule implements ProcessingRule {

    private final TypeUtils mTypeUtils;

    private final CompileGraph mCompileGraph;

    RelationRule(ProcessingEnvironment processingEnv, CompileGraph compileGraph) {
        mTypeUtils = new TypeUtils(processingEnv);
        mCompileGraph = compileGraph;
    }

    private static ClassName makeClassName(Element field) {
        final String fieldName = field.getSimpleName().toString();
        final String suffix;
        final Matcher matcher = ColumnRule.HUNGARIAN_NOTATION.matcher(fieldName);
        if (matcher.matches()) {
            suffix = toUnderScope(matcher.group(1));
        } else {
            suffix = toUnderScope(fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1));
        }
        final ClassName enclosingClass = ClassName.get((TypeElement) field.getEnclosingElement());
        return ClassName.get(enclosingClass.packageName(), enclosingClass.simpleName() + "_" + suffix);
    }

    private static String toUnderScope(String value) {
        return value.replaceAll("(.)(\\p{Upper})", "$1_$2");
    }

    @Override
    public void process(Element element) throws Exception {
        final Element enclosingElement = element.getEnclosingElement();
        if (enclosingElement.getAnnotation(Entry.class) == null) {
            throw new ElementException("Class containing @Relation must be annotated with @Entry", enclosingElement);
        }
        element.asType().accept(new SimpleTypeVisitor7<Void, Void>() {
            @Override
            public Void visitDeclared(DeclaredType t, Void unused) {
                final List<? extends TypeMirror> args = t.getTypeArguments();
                if (args.isEmpty()) {
                    processOneToOne(element, t.asElement());
                } else {
                    processOneToMany(element, (TypeElement) t.asElement(), args.get(0));
                }
                return super.visitDeclared(t, unused);
            }
        }, null);
    }

    private void processOneToOne(Element field, Element relation) {
        if (relation.getAnnotation(Entry.class) == null) {
            throw new ElementException("Related type must be annotated with @Entry", relation);
        }
        final Element enclosingElement = field.getEnclosingElement();
        final TableSpec lTable = mCompileGraph.findTableSpec(enclosingElement);
        final TableSpec rTable = mCompileGraph.findTableSpec(relation);
        final ClassName className = makeClassName(field);
        final RelationSpec relationSpec = new RelationSpec(
                field, className, lTable, rTable, false);
        mCompileGraph.putRelationSpec(enclosingElement, relationSpec);
    }

    private void processOneToMany(Element field, Element collectionType, TypeMirror relation) {
        if (!mTypeUtils.isAssignable(collectionType.asType(), List.class)) {
            throw new ElementException("Relation type must be subclass of List<E>", field);
        }
        relation.accept(new SimpleTypeVisitor7<Void, Void>() {
            @Override
            public Void visitDeclared(DeclaredType t, Void unused) {
                final Element element = t.asElement();
                if (element.getAnnotation(Entry.class) == null) {
                    throw new ElementException("Related type must be annotated with @Entry", element);
                }
                final Element enclosingElement = field.getEnclosingElement();
                final TableSpec lTable = mCompileGraph.findTableSpec(enclosingElement);
                final TableSpec rTable = mCompileGraph.findTableSpec(element);
                final ClassName className = makeClassName(field);
                final RelationSpec relationSpec = new RelationSpec(
                        field, className, lTable, rTable, true);
                mCompileGraph.putRelationSpec(enclosingElement, relationSpec);
                return super.visitDeclared(t, unused);
            }
        }, null);
    }

}