/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.netbeans.modules.classfile;

import java.io.DataInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * The JDK 9 Module attribute.
 * @since 1.52
 * @author Tomas Zezula
 */
public final class Module {

    private final String name;
    private final int flags;
    private final String version;
    private final List<RequiresEntry> requires;
    private final List<ExportsEntry> exports;
    private final List<OpensEntry> opens;
    private final List<ClassName> uses;
    private final List<ProvidesEntry> provides;

    Module(final DataInputStream in, final ConstantPool cp) throws IOException {
        final CPEntry entry = cp.get(in.readUnsignedShort());
        if (entry.getTag() != ConstantPool.CONSTANT_Module) {
            throw new LegacyClassFile("Java 9 older than b148.");   //NOI18N
        }
        name = ((CPModuleInfo)entry).getName();
        flags = in.readUnsignedShort();
        int index = in.readUnsignedShort();
        version = index == 0 ?
                null :
                ((CPUTF8Info)cp.get(index)).getName();
        final int reqCnt = in.readUnsignedShort();
        final RequiresEntry[] req = new RequiresEntry[reqCnt];
        for (int i=0; i<reqCnt; i++) {
            req[i] = new RequiresEntry(in, cp);
        }
        requires = Collections.unmodifiableList(Arrays.asList(req));
        final int expCnt = in.readUnsignedShort();
        final ExportsEntry[] exp = new ExportsEntry[expCnt];
        for (int i=0; i<expCnt; i++) {
            exp[i] = new ExportsEntry(in, cp);
        }
        exports = Collections.unmodifiableList(Arrays.asList(exp));
        final int opnCnt = in.readUnsignedShort();
        final OpensEntry[] opn = new OpensEntry[opnCnt];
        for (int i=0; i<opnCnt; i++) {
            opn[i] = new OpensEntry(in, cp);
        }
        opens = Collections.unmodifiableList(Arrays.asList(opn));
        final int usesCnt = in.readUnsignedShort();
        final ClassName[] uss = new ClassName[usesCnt];
        for (int i=0; i<usesCnt; i++) {
            uss[i] = ((CPClassInfo)cp.get(in.readUnsignedShort())).getClassName();
        }
        uses = Collections.unmodifiableList(Arrays.asList(uss));
        final int provCnt = in.readUnsignedShort();
        final ProvidesEntry[] prov = new ProvidesEntry[provCnt];
        for (int i=0; i< provCnt; i++) {
            prov[i] = new ProvidesEntry(in, cp);
        }
        provides = Collections.unmodifiableList(Arrays.asList(prov));
    }

    /**
     * Returns module name.
     * @return the module name
     */
    public String getName() {
        return name;
    }

    /**
     * Returns module flags.
     * @return the module flags
     */
    public int getFlags() {
        return flags;
    }

    /**
     * Returns module version.
     * @return the module version or null if not specified
     */
    public String getVersion() {
        return version;
    }

    /**
     * Returns the required modules.
     * @return the list of {@link RequiresEntry}
     */
    public List<RequiresEntry> getRequiresEntries() {
        return requires;
    }

    /**
     * Returns the exported packages.
     * @return the list of {@link ExportsEntry}
     */
    public List<ExportsEntry> getExportsEntries() {
        return exports;
    }

    /**
     * Returns the opened packages.
     * @return the list of {@link OpensEntry}
     */
    public List<OpensEntry> getOpensEntries() {
        return opens;
    }

    /**
     * Returns the used services.
     * @return the list of services used by this module
     */
    public List<ClassName> getUses() {
        return uses;
    }

    /**
     * Returns the provided services.
     * @return the list of {@link ProvidesEntry}
     */
    public List<ProvidesEntry> getProvidesEntries() {
        return provides;
    }

    /**
     * Required module
     */
    public static final class RequiresEntry {

        private final String module;
        private final int flags;
        private final String version;

        RequiresEntry(final DataInputStream in, final ConstantPool cp) throws IOException {
            module = ((CPModuleInfo)cp.get(in.readUnsignedShort())).getName();
            flags = in.readUnsignedShort();
            int index = in.readUnsignedShort();
            version = index == 0 ?
                    null :
                    ((CPUTF8Info)cp.get(index)).getName();
        }

        /**
         * Returns the module name.
         * @return the module name
         */
        public String getModule() {
            return module;
        }

        /**
         * Returns require modifiers.
         * @return flags
         */
        public int getFlags() {
            return flags;
        }

        /**
         * Returns the version of the required module.
         * @return version of required module or null if not specified
         */
        public String getVersion() {
            return version;
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("requires");
            if ((flags & Access.TRANSITIVE) != 0) {
                sb.append(" transitive");           //NOI18N
            }
            if ((flags & Access.STATIC_PHASE) != 0) {
                sb.append(" static");           //NOI18N
            }
            if ((flags & Access.SYNTHETIC) != 0) {
                sb.append(" synthetic");        //NOI18N
            }
            if ((flags & Access.MANDATED) != 0) {
                sb.append(" mandated");         //NOI18N
            }
            sb.append(' ')
                    .append(module);
            if (version != null) {
                sb.append("@")
                    .append(version);
            }
            return sb.toString();
        }
    }

    /**
     * Exported package.
     */
    public static final class ExportsEntry {
        private final String pkg;
        private final int flags;
        private final List<String> to;

        ExportsEntry(final DataInputStream in, final ConstantPool cp) throws IOException {
            pkg = ((CPPackageInfo) cp.get(in.readUnsignedShort())).getName();
            flags = in.readUnsignedShort();
            final int toCnt = in.readUnsignedShort();
            final String[] t = new String[toCnt];
            for (int i=0; i< toCnt; i++) {
                t[i] = ((CPModuleInfo) cp.get(in.readUnsignedShort())).getName();
            }
            to = Collections.unmodifiableList(Arrays.asList(t));
        }

        /**
         * Name of exported package.
         * @return the package name
         */
        public String getPackage() {
            return pkg;
        }

        /**
         * Returns export modifiers.
         * @return flags
         */
        public int getFlags() {
            return flags;
        }

        /**
         * Returns a list of modules to which the package is exported.
         * @return module list.
         */
        public List<String> getExportsTo() {
            return to;
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("exports");
            if ((flags & Access.SYNTHETIC) != 0) {
                sb.append(" synthetic");        //NOI18N
            }
            if ((flags & Access.MANDATED) != 0) {
                sb.append(" mandated");         //NOI18N
            }
            sb.append(' ')
                .append(pkg);
            if (!to.isEmpty()) {
                sb.append(" to ");                //NOI18N
                boolean first = true;
                for (String m : to) {
                    if (!first) {
                        sb.append(", ");        //NOI18N
                    } else {
                        first = false;
                    }
                    sb.append(m);
                }
            }
            return sb.toString();
        }
    }

    /**
     * Opened package.
     */
    public static final class OpensEntry {
        private final String pkg;
        private final int flags;
        private final List<String> to;

        OpensEntry(final DataInputStream in, final ConstantPool cp) throws IOException {
            pkg = ((CPPackageInfo) cp.get(in.readUnsignedShort())).getName();
            flags = in.readUnsignedShort();
            final int toCnt = in.readUnsignedShort();
            final String[] t = new String[toCnt];
            for (int i=0; i< toCnt; i++) {
                t[i] = ((CPModuleInfo) cp.get(in.readUnsignedShort())).getName();
            }
            to = Collections.unmodifiableList(Arrays.asList(t));
        }

        /**
         * Name of opened package.
         * @return the package name
         */
        public String getPackage() {
            return pkg;
        }

        /**
         * Returns open modifiers.
         * @return flags
         */
        public int getFlags() {
            return flags;
        }

        /**
         * Returns a list of modules to which the package is opened.
         * @return module list.
         */
        public List<String> getOpensTo() {
            return to;
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("opens");
            if ((flags & Access.SYNTHETIC) != 0) {
                sb.append(" synthetic");        //NOI18N
            }
            if ((flags & Access.MANDATED) != 0) {
                sb.append(" mandated");         //NOI18N
            }
            sb.append(' ')
                .append(pkg);
            if (!to.isEmpty()) {
                sb.append(" to ");                //NOI18N
                boolean first = true;
                for (String m : to) {
                    if (!first) {
                        sb.append(", ");        //NOI18N
                    } else {
                        first = false;
                    }
                    sb.append(m);
                }
            }
            return sb.toString();
        }
    }

    /**
     * Provided service.
     */
    public static final class ProvidesEntry {
        private final ClassName service;
        private final List<ClassName> impls;

        ProvidesEntry(final DataInputStream in, final ConstantPool cp) throws IOException {
            service = ((CPClassInfo)cp.get(in.readUnsignedShort())).getClassName();
            final int cnt = in.readUnsignedShort();
            final ClassName[] ims = new ClassName[cnt];
            for (int i=0; i< cnt; i++) {
                ims[i] = ((CPClassInfo)cp.get(in.readUnsignedShort())).getClassName();
            }
            impls = Collections.unmodifiableList(Arrays.asList(ims));
        }

        /**
         * Service type.
         * @return the service type
         */
        public ClassName getService() {
            return service;
        }

        /**
         * Service implementation.
         * @return the class implementing the service
         */
        public List<ClassName> getImplementations() {
            return impls;
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder()
                .append("provides ")            //NOI18N
                .append(service);
            if (!impls.isEmpty()) {
                sb.append(" with ");            //NOI18N
                boolean first = true;
                for (ClassName m : impls) {
                    if (!first) {
                        sb.append(", ");        //NOI18N
                    } else {
                        first = false;
                    }
                    sb.append(m.getExternalName());
                }
            }
            return sb.toString();
        }
    }
}
