package org.benf.cfr.reader.entities.attributes;

import org.benf.cfr.reader.bytecode.analysis.parse.literal.TypedLiteral;
import org.benf.cfr.reader.bytecode.analysis.parse.utils.Pair;
import org.benf.cfr.reader.bytecode.analysis.types.JavaTypeInstance;
import org.benf.cfr.reader.bytecode.analysis.types.RawJavaType;
import org.benf.cfr.reader.entities.annotations.*;
import org.benf.cfr.reader.entities.constantpool.ConstantPool;
import org.benf.cfr.reader.entities.constantpool.ConstantPoolEntry;
import org.benf.cfr.reader.entities.constantpool.ConstantPoolEntryUTF8;
import org.benf.cfr.reader.entities.constantpool.ConstantPoolUtils;
import org.benf.cfr.reader.util.ConfusedCFRException;
import org.benf.cfr.reader.util.collections.ListFactory;
import org.benf.cfr.reader.util.collections.MapFactory;
import org.benf.cfr.reader.util.bytestream.ByteData;

import java.util.List;
import java.util.Map;

class AnnotationHelpers {

    static Pair<Long, AnnotationTableEntry> getAnnotation(ByteData raw, long offset, ConstantPool cp) {
        ConstantPoolEntryUTF8 typeName = cp.getUTF8Entry(raw.getU2At(offset));
        offset += 2;
        int numElementPairs = raw.getU2At(offset);
        offset += 2;
        Map<String, ElementValue> elementValueMap = MapFactory.newOrderedMap();
        for (int x = 0; x < numElementPairs; ++x) {
            offset = getElementValuePair(raw, offset, cp, elementValueMap);
        }
        return new Pair<Long, AnnotationTableEntry>(offset, new AnnotationTableEntry(ConstantPoolUtils.decodeTypeTok(typeName.getValue(), cp), elementValueMap));
    }

    private static long getElementValuePair(ByteData raw, long offset, ConstantPool cp, Map<String, ElementValue> res) {
        ConstantPoolEntryUTF8 elementName = cp.getUTF8Entry(raw.getU2At(offset));
        offset += 2;
        Pair<Long, ElementValue> elementValueP = getElementValue(raw, offset, cp);
        offset = elementValueP.getFirst();
        res.put(elementName.getValue(), elementValueP.getSecond());
        return offset;
    }

    static Pair<Long, ElementValue> getElementValue(ByteData raw, long offset, ConstantPool cp) {

        char c = (char) raw.getU1At(offset);
        offset++;
        switch (c) {
            case 'B':
            case 'C':
            case 'D':
            case 'F':
            case 'I':
            case 'J':
            case 'S':
            case 'Z': {
                RawJavaType rawJavaType = ConstantPoolUtils.decodeRawJavaType(c);
                ConstantPoolEntry constantPoolEntry = cp.getEntry(raw.getU2At(offset));
                TypedLiteral typedLiteral = TypedLiteral.getConstantPoolEntry(cp, constantPoolEntry);
                ElementValue value = new ElementValueConst(typedLiteral);
                value = value.withTypeHint(rawJavaType);
                return new Pair<Long, ElementValue>(offset + 2, value);
            }
            case 's': {
                ConstantPoolEntry constantPoolEntry = cp.getEntry(raw.getU2At(offset));
                TypedLiteral typedLiteral = TypedLiteral.getConstantPoolEntryUTF8((ConstantPoolEntryUTF8) constantPoolEntry);
                return new Pair<Long, ElementValue>(offset + 2, new ElementValueConst(typedLiteral));
            }
            case 'e': {
                ConstantPoolEntryUTF8 enumClassName = cp.getUTF8Entry(raw.getU2At(offset));
                ConstantPoolEntryUTF8 enumEntryName = cp.getUTF8Entry(raw.getU2At(offset + 2));
                return new Pair<Long, ElementValue>(offset + 4, new ElementValueEnum(ConstantPoolUtils.decodeTypeTok(enumClassName.getValue(), cp), enumEntryName.getValue()));
            }
            case 'c': {
                ConstantPoolEntryUTF8 className = cp.getUTF8Entry(raw.getU2At(offset));
                String typeName = className.getValue();
                if (typeName.equals("V")) {
                    return new Pair<Long, ElementValue>(offset + 2, new ElementValueClass(RawJavaType.VOID));
                } else {
                    return new Pair<Long, ElementValue>(offset + 2, new ElementValueClass(ConstantPoolUtils.decodeTypeTok(typeName, cp)));
                }
            }
            case '@': {
                Pair<Long, AnnotationTableEntry> ape = getAnnotation(raw, offset, cp);
                return new Pair<Long, ElementValue>(ape.getFirst(), new ElementValueAnnotation(ape.getSecond()));
            }
            case '[': {
                int numArrayEntries = raw.getU2At(offset);
                offset += 2;
                List<ElementValue> res = ListFactory.newList();
                for (int x = 0; x < numArrayEntries; ++x) {
                    Pair<Long, ElementValue> ape = getElementValue(raw, offset, cp);
                    offset = ape.getFirst();
                    res.add(ape.getSecond());
                }
                return new Pair<Long, ElementValue>(offset, new ElementValueArray(res));
            }
            default:
                throw new ConfusedCFRException("Illegal attribute tag [" + c + "]");
        }
    }

    static Pair<Long, AnnotationTableTypeEntry> getTypeAnnotation(ByteData raw, long offset, ConstantPool cp) {
        short targetType = raw.getU1At(offset++);
        TypeAnnotationEntryValue typeAnnotationEntryValue = TypeAnnotationEntryValue.get(targetType);
        Pair<Long, TypeAnnotationTargetInfo> targetInfoPair = readTypeAnnotationTargetInfo(typeAnnotationEntryValue.getKind(), raw, offset);
        offset = targetInfoPair.getFirst();
        TypeAnnotationTargetInfo targetInfo = targetInfoPair.getSecond();

        short type_path_length = raw.getU1At(offset++);
        List<TypePathPart> pathData = ListFactory.newList();
        for (int x=0;x<type_path_length;++x) {
            short type_path_kind = raw.getU1At(offset++);
            short type_argument_index = raw.getU1At(offset++);
            switch (type_path_kind) {
                case 0:
                    pathData.add(TypePathPartArray.INSTANCE);
                    break;
                case 1:
                    pathData.add(TypePathPartNested.INSTANCE);
                    break;
                case 2:
                    pathData.add(TypePathPartBound.INSTANCE);
                    break;
                case 3:
                    pathData.add(new TypePathPartParameterized(type_argument_index));
                    break;
            }
        }
        TypePath path = new TypePath(pathData);

        int type_index = raw.getU2At(offset);
        offset+=2;
        ConstantPoolEntryUTF8 type_entry = cp.getUTF8Entry(type_index);
        JavaTypeInstance type = ConstantPoolUtils.decodeTypeTok(type_entry.getValue(), cp);


        int numElementPairs = raw.getU2At(offset);
        offset += 2;
        Map<String, ElementValue> elementValueMap = MapFactory.newOrderedMap();
        for (int x = 0; x < numElementPairs; ++x) {
            offset = getElementValuePair(raw, offset, cp, elementValueMap);
        }

        AnnotationTableTypeEntry res = new AnnotationTableTypeEntry<TypeAnnotationTargetInfo>(typeAnnotationEntryValue, targetInfo, path, type, elementValueMap);

        return new Pair<Long, AnnotationTableTypeEntry>(offset, res);
    }

    // The spec claims that target_info is a union, however localvar_target is a variable length structure....
    private static Pair<Long, TypeAnnotationTargetInfo> readTypeAnnotationTargetInfo(TypeAnnotationEntryKind kind, ByteData raw, long offset) {
        switch (kind) {
            case type_parameter_target:
                return TypeAnnotationTargetInfo.TypeAnnotationParameterTarget.Read(raw, offset);
            case supertype_target:
                return TypeAnnotationTargetInfo.TypeAnnotationSupertypeTarget.Read(raw, offset);
            case type_parameter_bound_target:
                return TypeAnnotationTargetInfo.TypeAnnotationParameterBoundTarget.Read(raw, offset);
            case empty_target:
                return TypeAnnotationTargetInfo.TypeAnnotationEmptyTarget.Read(raw, offset);
            case method_formal_parameter_target:
                return TypeAnnotationTargetInfo.TypeAnnotationFormalParameterTarget.Read(raw, offset);
            case throws_target:
                return TypeAnnotationTargetInfo.TypeAnnotationThrowsTarget.Read(raw, offset);
            case localvar_target:
                return TypeAnnotationTargetInfo.TypeAnnotationLocalVarTarget.Read(raw, offset);
            case catch_target:
                return TypeAnnotationTargetInfo.TypeAnnotationCatchTarget.Read(raw, offset);
            case offset_target:
                return TypeAnnotationTargetInfo.TypeAnnotationOffsetTarget.Read(raw, offset);
            case type_argument_target:
                return TypeAnnotationTargetInfo.TypeAnnotationTypeArgumentTarget.Read(raw, offset);
            default:
                throw new BadAttributeException();
        }
    }

}