/*
 * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.internal.classfile.impl;

import java.lang.classfile.*;
import java.lang.classfile.attribute.*;
import java.lang.classfile.constantpool.ClassEntry;
import java.lang.classfile.constantpool.ConstantPool;
import java.lang.reflect.AccessFlag;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;

import jdk.internal.access.SharedSecrets;

public final class ClassImpl
        extends AbstractElement
        implements ClassModel {
    final ClassReaderImpl reader;
    private final int attributesPos;
    private final List<MethodModel> methods;
    private final List<FieldModel> fields;
    private List<Attribute<?>> attributes;
    private List<ClassEntry> interfaces;

    public ClassImpl(byte[] cfbytes, ClassFileImpl context) {
        this.reader = new ClassReaderImpl(cfbytes, context);
        int p = reader.interfacesPos;
        int icnt = reader.readU2(p);
        p += 2 + icnt * 2;
        int fcnt = reader.readU2(p);
        FieldImpl[] fields = new FieldImpl[fcnt];
        p += 2;
        for (int i = 0; i < fcnt; ++i) {
            int startPos = p;
            int attrStart = p + 6;
            p = reader.skipAttributeHolder(attrStart);
            fields[i] = new FieldImpl(reader, startPos, p, attrStart);
        }
        this.fields = List.of(fields);
        int mcnt = reader.readU2(p);
        MethodImpl[] methods = new MethodImpl[mcnt];
        p += 2;
        for (int i = 0; i < mcnt; ++i) {
            int startPos = p;
            int attrStart = p + 6;
            p = reader.skipAttributeHolder(attrStart);
            methods[i] = new MethodImpl(reader, startPos, p, attrStart);
        }
        this.methods = List.of(methods);
        this.attributesPos = p;
        reader.setContainedClass(this);
    }

    public int classfileLength() {
        return reader.classfileLength();
    }

    @Override
    public AccessFlags flags() {
        return new AccessFlagsImpl(AccessFlag.Location.CLASS, reader.flags());
    }

    @Override
    public int majorVersion() {
        return reader.readU2(6);
    }

    @Override
    public int minorVersion() {
        return reader.readU2(4);
    }

    @Override
    public ConstantPool constantPool() {
        return reader;
    }

    @Override
    public ClassEntry thisClass() {
        return reader.thisClassEntry();
    }

    @Override
    public Optional<ClassEntry> superclass() {
        return reader.superclassEntry();
    }

    @Override
    public List<ClassEntry> interfaces() {
        if (interfaces == null) {
            int pos = reader.thisClassPos() + 4;
            int cnt = reader.readU2(pos);
            pos += 2;
            var arr = new Object[cnt];
            for (int i = 0; i < cnt; ++i) {
                arr[i] = reader.readEntry(pos, ClassEntry.class);
                pos += 2;
            }
            this.interfaces = SharedSecrets.getJavaUtilCollectionAccess().listFromTrustedArray(arr);
        }
        return interfaces;
    }

    @Override
    public List<Attribute<?>> attributes() {
        if (attributes == null) {
            attributes = BoundAttribute.readAttributes(this, reader, attributesPos, reader.customAttributes());
        }
        return attributes;
    }

    // ClassModel

    @Override
    public void forEach(Consumer<? super ClassElement> consumer) {
        consumer.accept(flags());
        consumer.accept(ClassFileVersion.of(majorVersion(), minorVersion()));
        superclass().ifPresent(new Consumer<ClassEntry>() {
            @Override
            public void accept(ClassEntry entry) {
                consumer.accept(Superclass.of(entry));
            }
        });
        consumer.accept(Interfaces.of(interfaces()));
        fields().forEach(consumer);
        methods().forEach(consumer);
        for (Attribute<?> attr : attributes()) {
            if (attr instanceof ClassElement e)
                consumer.accept(e);
        }
    }

    @Override
    public List<FieldModel> fields() {
        return fields;
    }

    @Override
    public List<MethodModel> methods() {
        return methods;
    }

    @Override
    public boolean isModuleInfo() {
        AccessFlags flags = flags();
        // move to where?
        return flags.has(AccessFlag.MODULE)
               && majorVersion() >= ClassFile.JAVA_9_VERSION
               && thisClass().asInternalName().equals("module-info")
               && (superclass().isEmpty())
               && interfaces().isEmpty()
               && fields().isEmpty()
               && methods().isEmpty()
               && verifyModuleAttributes();
    }

    @Override
    public String toString() {
        return String.format("ClassModel[thisClass=%s, flags=%d]", thisClass().name().stringValue(), flags().flagsMask());
    }

    private boolean verifyModuleAttributes() {
        if (findAttribute(Attributes.module()).isEmpty())
            return false;

        return attributes().stream().allMatch(a ->
                a instanceof ModuleAttribute
             || a instanceof ModulePackagesAttribute
             || a instanceof ModuleHashesAttribute
             || a instanceof ModuleMainClassAttribute
             || a instanceof ModuleResolutionAttribute
             || a instanceof ModuleTargetAttribute
             || a instanceof InnerClassesAttribute
             || a instanceof SourceFileAttribute
             || a instanceof SourceDebugExtensionAttribute
             || a instanceof RuntimeVisibleAnnotationsAttribute
             || a instanceof RuntimeInvisibleAnnotationsAttribute
             || a instanceof CustomAttribute);
    }
}
