package de.renew.plugin.jpms.impl;

import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;


/**
 * A collection of packages that shall be exported to a target module whose
 * configuration  hasn't been resolved yet and with no layer containing  it
 * i.e. unresolved qualified exports. Such exports are ignored by the  JPMS
 * and  discarded  at  runtime when a Module A with  qualified  exports  to
 * another unresolved Module B gets resolved. More precisely:
 * <ul>
 *     <li>If  A and B are in the same module layer or if A is in  a  child
 *     layer to that of B the exports can be resolved.</li>
 *     <li>If  A's  layer  is  a parent of B's  layer,  the  exports  could
 *     normally  not  be  resolved due to the fact that  A  must have  been
 *     resolved before B.</li>
 * </ul>
 * The  {@link ModuleManager}  addresses  this by keeping  track  of  these
 * exports  and  later  resolving them once source and  target  module  are
 * loaded.
 *
 * @author Kjell Ehlers
 * @since Renew 4.2
 * @see ResolveExportsStrategy
 */
final class UnresolvedExports implements Comparable<UnresolvedExports> {
    private final String _target;
    private final Set<String> _packages;

    private boolean _fromBoot;
    private WeakReference<ModuleLayer.Controller> _srcControl;

    private UnresolvedExports(final String target) {
        _target = target;
        _packages = new HashSet<>();
    }

    /**
     * Creates a new empty {@code UnresolvedExports} for the given target.
     *
     * @param  target the module name of this exports' target.
     * @return A  new {@code UnresolvedExports} with the  specified  target
     *         and no source and no packages to export.
     * @throws NullPointerException if the target argument is {@code null}.
     */
    public static UnresolvedExports withTarget(final String target) throws NullPointerException {
        return new UnresolvedExports(Objects.requireNonNull(target));
    }

    /**
     * Sets  the  source module layer's controller, that shall be  used  in
     * resolving  this {@code UnresolvedExports}. The corresponding  source
     * layer's modules must contain the exported packages.
     * <p>
     * Changing  the controller invalidates this  {@code UnresolvedExports}
     * thereby clearing the exported packages.
     *
     * @param  control the source module layer's controller. The controller
     *                 is  only weakly referenced and thus not kept  alive.
     *                 Users  must  ensure strengthening  of  the  referent
     *                 themselves.  Trying  to assign the  same  controller
     *                 again is a no-op.
     * @return This {@code UnresolvedExports} object.
     */
    public UnresolvedExports from(final ModuleLayer.Controller control) {
        if (_srcControl == null || !_srcControl.refersTo(control)) {
            _packages.clear();
            _srcControl = new WeakReference<>(control);
        }
        _fromBoot = false;
        return this;
    }

    UnresolvedExports fromBoot() {
        if (!_fromBoot) {
            _srcControl = new WeakReference<>(null);
            _packages.clear();
            _fromBoot = true;
        }
        return this;
    }

    /**
     * Adds the set of packages to this {@code UnresolvedExports}.
     *
     * @param  packages the  set of packages to add. A package's name  must
     *                  be  unique,  may not be null, empty or  consist  of
     *                  only  whitespaces.  This method is a no-op  if  the
     *                  argument is {@code null} or no package conforms  to
     *                  the conditions specified here.
     * @return This {@code UnresolvedExports} object.
     */
    public UnresolvedExports addPackages(final Set<String> packages) {
        for (String pkg : packages) {
            if (pkg != null && !pkg.isBlank()) {
                _packages.add(pkg);
            }
        }
        return this;
    }

    /**
     * Tries to resolve the exported packages with the given target module.
     * <p>
     * If the source controller is no longer alive or there are no exported
     * packages this effectively becomes a no-op. May not be called without
     * having set a source.
     *
     * @param  target the target module obj this  {@code UnresolvedExports}
     *                should be resolved with. May not be {@code null}.
     * @throws IllegalStateException if  no source was specified  for  this
     *         {@code UnresolvedExports}.
     */
    public void resolve(final Module target) throws IllegalStateException {
        if (_srcControl == null) {
            throw new IllegalStateException("No source specified for the packages.");
        }

        if (_packages.isEmpty()) {
            return;
        }

        ModuleLayer.Controller control = _srcControl.get();
        if (control != null) {
            for (Module m : control.layer().modules()) {
                m.getPackages().stream().filter(_packages::contains)
                    .forEach(pkg -> control.addExports(m, pkg, target));
            }
        } else if (_fromBoot) {
            Module loader = getClass().getModule();
            loader.getPackages().stream().filter(_packages::contains)
                .forEach(pkg -> loader.addExports(pkg, target));
        }
    }

    /**
     * Returns this {@code UnresolvedExports}' target module.
     *
     * @return The target module's name.
     */
    public String target() {
        return _target;
    }

    @Override
    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        }

        if (!(o instanceof UnresolvedExports other)) {
            return false;
        }

        return target().equals(other.target()) && _packages.equals(other._packages);
    }

    @Override
    public int hashCode() {
        return Objects.hash(target(), _packages);
    }

    @Override
    public int compareTo(final UnresolvedExports o) {
        if (o == this) {
            return 0;
        }

        int c = target().compareTo(o.target());
        if (c != 0) {
            return c;
        }

        String[] a = _packages.toArray(new String[0]);
        String[] b = o._packages.toArray(new String[0]);
        Arrays.sort(a);
        Arrays.sort(b);

        return Arrays.compare(a, b);
    }
}
