/**
 * Copyright (c) 2016, All partners of the iTesla project (http://www.itesla-project.eu/consortium)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
package com.powsybl.iidm.network.impl;

import com.google.common.collect.*;
import com.google.common.primitives.Ints;
import com.powsybl.commons.PowsyblException;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.Branch.Side;
import com.powsybl.iidm.network.impl.util.RefChain;
import com.powsybl.iidm.network.impl.util.RefObj;
import com.powsybl.math.graph.GraphUtil;
import com.powsybl.math.graph.GraphUtil.ConnectedComponentsComputationResult;
import gnu.trove.list.array.TIntArrayList;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 *
 * @author Geoffroy Jamgotchian <geoffroy.jamgotchian at rte-france.com>
 */
class NetworkImpl extends AbstractIdentifiable<Network> implements Network, VariantManagerHolder, MultiVariantObject {

    private static final Logger LOGGER = LoggerFactory.getLogger(NetworkImpl.class);

    private final RefChain<NetworkImpl> ref = new RefChain<>(new RefObj<>(this));

    private DateTime caseDate = new DateTime(); // default is the time at which the network has been created

    private int forecastDistance = 0;

    private String sourceFormat;

    private final NetworkIndex index = new NetworkIndex();

    private final VariantManagerImpl variantManager;

    private final NetworkListenerList listeners = new NetworkListenerList();

    class BusBreakerViewImpl implements BusBreakerView {

        @Override
        public Iterable<Bus> getBuses() {
            return FluentIterable.from(getVoltageLevels())
                    .transformAndConcat(vl -> vl.getBusBreakerView().getBuses());
        }

        @Override
        public Stream<Bus> getBusStream() {
            return getVoltageLevelStream().flatMap(vl -> vl.getBusBreakerView().getBusStream());
        }

        @Override
        public Iterable<Switch> getSwitches() {
            return FluentIterable.from(getVoltageLevels())
                    .transformAndConcat(vl -> vl.getBusBreakerView().getSwitches());
        }

        @Override
        public Stream<Switch> getSwitchStream() {
            return getVoltageLevelStream().flatMap(vl -> vl.getBusBreakerView().getSwitchStream());
        }

        @Override
        public int getSwitchCount() {
            return getVoltageLevelStream().mapToInt(vl -> vl.getBusBreakerView().getSwitchCount()).sum();
        }

        @Override
        public Bus getBus(String id) {
            return getVoltageLevelStream().map(vl -> vl.getBusBreakerView().getBus(id))
                    .filter(Objects::nonNull)
                    .findFirst()
                    .orElse(null);
        }
    }

    private final BusBreakerViewImpl busBreakerView = new BusBreakerViewImpl();

    class BusViewImpl implements BusView {

        @Override
        public Iterable<Bus> getBuses() {
            return FluentIterable.from(getVoltageLevels())
                    .transformAndConcat(vl -> vl.getBusView().getBuses());
        }

        @Override
        public Stream<Bus> getBusStream() {
            return getVoltageLevelStream().flatMap(vl -> vl.getBusView().getBusStream());
        }

        @Override
        public Collection<Component> getConnectedComponents() {
            return Collections.unmodifiableList(variants.get().connectedComponentsManager.getConnectedComponents());
        }

        @Override
        public Bus getBus(String id) {
            return getVoltageLevelStream().map(vl -> vl.getBusView().getBus(id))
                    .filter(Objects::nonNull)
                    .findFirst()
                    .orElse(null);
        }

    }

    private final BusViewImpl busView = new BusViewImpl();

    NetworkImpl(String id, String name, String sourceFormat) {
        super(id, name);
        Objects.requireNonNull(sourceFormat, "source format is null");
        this.sourceFormat = sourceFormat;
        variantManager = new VariantManagerImpl(this);
        variants = new VariantArray<>(ref, VariantImpl::new);
        // add the network the object list as it is a multi variant object
        // and it needs to be notified when and extension or a reduction of
        // the variant array is requested
        index.checkAndAdd(this);
    }

    @Override
    public ContainerType getContainerType() {
        return ContainerType.NETWORK;
    }

    @Override
    public DateTime getCaseDate() {
        return caseDate;
    }

    @Override
    public NetworkImpl setCaseDate(DateTime caseDate) {
        ValidationUtil.checkCaseDate(this, caseDate);
        this.caseDate = caseDate;
        return this;
    }

    @Override
    public int getForecastDistance() {
        return forecastDistance;
    }

    @Override
    public NetworkImpl setForecastDistance(int forecastDistance) {
        ValidationUtil.checkForecastDistance(this, forecastDistance);
        this.forecastDistance = forecastDistance;
        return this;
    }

    @Override
    public String getSourceFormat() {
        return sourceFormat;
    }

    RefChain<NetworkImpl> getRef() {
        return ref;
    }

    NetworkListenerList getListeners() {
        return listeners;
    }

    public NetworkIndex getIndex() {
        return index;
    }

    @Override
    public NetworkImpl getNetwork() {
        return this;
    }

    @Override
    public VariantManagerImpl getVariantManager() {
        return variantManager;
    }

    @Override
    public int getVariantIndex() {
        return variantManager.getVariantContext().getVariantIndex();
    }

    @Override
    public Set<Country> getCountries() {
        return getSubstationStream()
                .map(Substation::getCountry)
                .filter(Optional::isPresent).map(Optional::get)
                .collect(Collectors.toCollection(() -> EnumSet.noneOf(Country.class)));
    }

    @Override
    public int getCountryCount() {
        return getCountries().size();
    }

    @Override
    public SubstationAdder newSubstation() {
        return new SubstationAdderImpl(ref);
    }

    @Override
    public Iterable<Substation> getSubstations() {
        return Collections.unmodifiableCollection(index.getAll(SubstationImpl.class));
    }

    @Override
    public Stream<Substation> getSubstationStream() {
        return index.getAll(SubstationImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getSubstationCount() {
        return index.getAll(SubstationImpl.class).size();
    }

    @Override
    public Iterable<Substation> getSubstations(Country country, String tsoId, String... geographicalTags) {
        return getSubstations(Optional.ofNullable(country).map(Country::getName).orElse(null), tsoId, geographicalTags);
    }

    @Override
    public Iterable<Substation> getSubstations(String country, String tsoId, String... geographicalTags) {
        return Substations.filter(getSubstations(), country, tsoId, geographicalTags);
    }

    @Override
    public SubstationImpl getSubstation(String id) {
        return index.get(id, SubstationImpl.class);
    }

    @Override
    public Iterable<VoltageLevel> getVoltageLevels() {
        return Iterables.concat(index.getAll(BusBreakerVoltageLevel.class),
                index.getAll(NodeBreakerVoltageLevel.class));
    }

    @Override
    public Stream<VoltageLevel> getVoltageLevelStream() {
        return Stream.concat(index.getAll(BusBreakerVoltageLevel.class).stream(),
                index.getAll(NodeBreakerVoltageLevel.class).stream());
    }

    @Override
    public int getVoltageLevelCount() {
        return index.getAll(BusBreakerVoltageLevel.class).size()
                + index.getAll(NodeBreakerVoltageLevel.class).size();
    }

    @Override
    public VoltageLevelExt getVoltageLevel(String id) {
        return index.get(id, VoltageLevelExt.class);
    }

    @Override
    public LineAdderImpl newLine() {
        return new LineAdderImpl(this);
    }

    @Override
    public Iterable<Line> getLines() {
        return Iterables.concat(index.getAll(LineImpl.class), index.getAll(TieLineImpl.class));
    }

    @Override
    public Branch getBranch(String branchId) {
        Objects.requireNonNull(branchId);
        Branch branch = getLine(branchId);
        if (branch == null) {
            branch = getTwoWindingsTransformer(branchId);
        }
        return branch;
    }

    @Override
    public Iterable<Branch> getBranches() {
        return Iterables.concat(getLines(), getTwoWindingsTransformers());
    }

    @Override
    public Stream<Branch> getBranchStream() {
        return Stream.concat(getLineStream(), getTwoWindingsTransformerStream());
    }

    @Override
    public int getBranchCount() {
        return getLineCount() + getTwoWindingsTransformerCount();
    }

    @Override
    public Stream<Line> getLineStream() {
        return Stream.concat(index.getAll(LineImpl.class).stream(), index.getAll(TieLineImpl.class).stream());
    }

    @Override
    public int getLineCount() {
        return index.getAll(LineImpl.class).size() + index.getAll(TieLineImpl.class).size();
    }

    @Override
    public LineImpl getLine(String id) {
        LineImpl line = index.get(id, LineImpl.class);
        if (line == null) {
            line = index.get(id, TieLineImpl.class);
        }
        return line;
    }

    @Override
    public TieLineAdderImpl newTieLine() {
        return new TieLineAdderImpl(this);
    }

    @Override
    public Iterable<TwoWindingsTransformer> getTwoWindingsTransformers() {
        return Collections.unmodifiableCollection(index.getAll(TwoWindingsTransformerImpl.class));
    }

    @Override
    public Stream<TwoWindingsTransformer> getTwoWindingsTransformerStream() {
        return index.getAll(TwoWindingsTransformerImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getTwoWindingsTransformerCount() {
        return index.getAll(TwoWindingsTransformerImpl.class).size();
    }

    @Override
    public TwoWindingsTransformer getTwoWindingsTransformer(String id) {
        return index.get(id, TwoWindingsTransformerImpl.class);
    }

    @Override
    public Iterable<ThreeWindingsTransformer> getThreeWindingsTransformers() {
        return Collections.unmodifiableCollection(index.getAll(ThreeWindingsTransformerImpl.class));
    }

    @Override
    public Stream<ThreeWindingsTransformer> getThreeWindingsTransformerStream() {
        return index.getAll(ThreeWindingsTransformerImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getThreeWindingsTransformerCount() {
        return index.getAll(ThreeWindingsTransformerImpl.class).size();
    }

    @Override
    public ThreeWindingsTransformer getThreeWindingsTransformer(String id) {
        return index.get(id, ThreeWindingsTransformerImpl.class);
    }

    @Override
    public Iterable<Generator> getGenerators() {
        return Collections.unmodifiableCollection(index.getAll(GeneratorImpl.class));
    }

    @Override
    public Stream<Generator> getGeneratorStream() {
        return index.getAll(GeneratorImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getGeneratorCount() {
        return index.getAll(GeneratorImpl.class).size();
    }

    @Override
    public GeneratorImpl getGenerator(String id) {
        return index.get(id, GeneratorImpl.class);
    }

    @Override
    public Iterable<Battery> getBatteries() {
        return Collections.unmodifiableCollection(index.getAll(BatteryImpl.class));
    }

    @Override
    public Stream<Battery> getBatteryStream() {
        return index.getAll(BatteryImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getBatteryCount() {
        return index.getAll(BatteryImpl.class).size();
    }

    @Override
    public BatteryImpl getBattery(String id) {
        return index.get(id, BatteryImpl.class);
    }

    @Override
    public Iterable<Load> getLoads() {
        return Collections.unmodifiableCollection(index.getAll(LoadImpl.class));
    }

    @Override
    public Stream<Load> getLoadStream() {
        return index.getAll(LoadImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getLoadCount() {
        return index.getAll(LoadImpl.class).size();
    }

    @Override
    public LoadImpl getLoad(String id) {
        return index.get(id, LoadImpl.class);
    }

    @Override
    public Iterable<ShuntCompensator> getShuntCompensators() {
        return Collections.unmodifiableCollection(index.getAll(ShuntCompensatorImpl.class));
    }

    @Override
    public Stream<ShuntCompensator> getShuntCompensatorStream() {
        return index.getAll(ShuntCompensatorImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getShuntCompensatorCount() {
        return index.getAll(ShuntCompensatorImpl.class).size();
    }

    @Override
    public ShuntCompensatorImpl getShuntCompensator(String id) {
        return index.get(id, ShuntCompensatorImpl.class);
    }

    @Override
    public Iterable<DanglingLine> getDanglingLines() {
        return Collections.unmodifiableCollection(index.getAll(DanglingLineImpl.class));
    }

    @Override
    public Stream<DanglingLine> getDanglingLineStream() {
        return index.getAll(DanglingLineImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getDanglingLineCount() {
        return index.getAll(DanglingLineImpl.class).size();
    }

    @Override
    public DanglingLineImpl getDanglingLine(String id) {
        return index.get(id, DanglingLineImpl.class);
    }

    @Override
    public Iterable<StaticVarCompensator> getStaticVarCompensators() {
        return Collections.unmodifiableCollection(index.getAll(StaticVarCompensatorImpl.class));
    }

    @Override
    public Stream<StaticVarCompensator> getStaticVarCompensatorStream() {
        return index.getAll(StaticVarCompensatorImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getStaticVarCompensatorCount() {
        return index.getAll(StaticVarCompensatorImpl.class).size();
    }

    @Override
    public StaticVarCompensatorImpl getStaticVarCompensator(String id) {
        return index.get(id, StaticVarCompensatorImpl.class);
    }

    @Override
    public Switch getSwitch(String id) {
        return index.get(id, SwitchImpl.class);
    }

    @Override
    public Iterable<Switch> getSwitches() {
        return Collections.unmodifiableCollection(index.getAll(SwitchImpl.class));
    }

    @Override
    public Stream<Switch> getSwitchStream() {
        return index.getAll(SwitchImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getSwitchCount() {
        return index.getAll(SwitchImpl.class).size();
    }

    @Override
    public BusbarSection getBusbarSection(String id) {
        return index.get(id, BusbarSectionImpl.class);
    }

    @Override
    public Iterable<BusbarSection> getBusbarSections() {
        return Collections.unmodifiableCollection(index.getAll(BusbarSectionImpl.class));
    }

    @Override
    public Stream<BusbarSection> getBusbarSectionStream() {
        return index.getAll(BusbarSectionImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getBusbarSectionCount() {
        return index.getAll(BusbarSectionImpl.class).size();
    }

    @Override
    public AbstractHvdcConverterStation<?> getHvdcConverterStation(String id) {
        AbstractHvdcConverterStation<?> converterStation = getLccConverterStation(id);
        if (converterStation == null) {
            converterStation = getVscConverterStation(id);
        }
        return converterStation;
    }

    @Override
    public int getHvdcConverterStationCount() {
        return getLccConverterStationCount() + getVscConverterStationCount();
    }

    @Override
    public Iterable<HvdcConverterStation<?>> getHvdcConverterStations() {
        return Iterables.concat(getLccConverterStations(), getVscConverterStations());
    }

    @Override
    public Stream<HvdcConverterStation<?>> getHvdcConverterStationStream() {
        return Stream.concat(getLccConverterStationStream(), getVscConverterStationStream());
    }

    @Override
    public Iterable<LccConverterStation> getLccConverterStations() {
        return Collections.unmodifiableCollection(index.getAll(LccConverterStationImpl.class));
    }

    @Override
    public Stream<LccConverterStation> getLccConverterStationStream() {
        return index.getAll(LccConverterStationImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getLccConverterStationCount() {
        return index.getAll(LccConverterStationImpl.class).size();
    }

    @Override
    public LccConverterStationImpl getLccConverterStation(String id) {
        return index.get(id, LccConverterStationImpl.class);
    }

    @Override
    public Iterable<VscConverterStation> getVscConverterStations() {
        return Collections.unmodifiableCollection(index.getAll(VscConverterStationImpl.class));
    }

    @Override
    public Stream<VscConverterStation> getVscConverterStationStream() {
        return index.getAll(VscConverterStationImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getVscConverterStationCount() {
        return index.getAll(VscConverterStationImpl.class).size();
    }

    @Override
    public VscConverterStationImpl getVscConverterStation(String id) {
        return index.get(id, VscConverterStationImpl.class);
    }

    @Override
    public HvdcLine getHvdcLine(String id) {
        return index.get(id, HvdcLineImpl.class);
    }

    @Override
    public HvdcLine getHvdcLine(HvdcConverterStation converterStation) {
        return getHvdcLineStream()
                .filter(l -> l.getConverterStation1() == converterStation || l.getConverterStation2() == converterStation)
                .findFirst()
                .orElse(null);
    }

    @Override
    public int getHvdcLineCount() {
        return index.getAll(HvdcLineImpl.class).size();
    }

    @Override
    public Iterable<HvdcLine> getHvdcLines() {
        return Collections.unmodifiableCollection(index.getAll(HvdcLineImpl.class));
    }

    @Override
    public Stream<HvdcLine> getHvdcLineStream() {
        return index.getAll(HvdcLineImpl.class).stream().map(Function.identity());
    }

    @Override
    public HvdcLineAdder newHvdcLine() {
        return new HvdcLineAdderImpl(ref);
    }

    @Override
    public Identifiable<?> getIdentifiable(String id) {
        return index.get(id, Identifiable.class);
    }

    @Override
    public Collection<Identifiable<?>> getIdentifiables() {
        return index.getAll();
    }

    @Override
    public <C extends Connectable> Iterable<C> getConnectables(Class<C> clazz) {
        return getConnectableStream(clazz).collect(Collectors.toList());
    }

    @Override
    public <C extends Connectable> Stream<C> getConnectableStream(Class<C> clazz) {
        return index.getAll().stream().filter(clazz::isInstance).map(clazz::cast);
    }

    @Override
    public <C extends Connectable> int getConnectableCount(Class<C> clazz) {
        return Ints.checkedCast(getConnectableStream(clazz).count());
    }

    @Override
    public Iterable<Connectable> getConnectables() {
        return getConnectables(Connectable.class);
    }

    @Override
    public Stream<Connectable> getConnectableStream() {
        return getConnectableStream(Connectable.class);
    }

    @Override
    public int getConnectableCount() {
        return Ints.checkedCast(getConnectableStream().count());
    }

    @Override
    public BusBreakerViewImpl getBusBreakerView() {
        return busBreakerView;
    }

    @Override
    public BusViewImpl getBusView() {
        return busView;
    }

    private abstract static class AbstractComponentsManager<C extends Component> {

        protected final NetworkImpl network;

        private AbstractComponentsManager(NetworkImpl network) {
            this.network = Objects.requireNonNull(network);
        }

        private List<C> components;

        void invalidate() {
            components = null;
        }

        protected void addToAdjacencyList(Bus bus1, Bus bus2, Map<String, Integer> id2num, TIntArrayList[] adjacencyList) {
            if (bus1 != null && bus2 != null) {
                int busNum1 = id2num.get(bus1.getId());
                int busNum2 = id2num.get(bus2.getId());
                adjacencyList[busNum1].add(busNum2);
                adjacencyList[busNum2].add(busNum1);
            }
        }

        protected void fillAdjacencyList(Map<String, Integer> id2num, TIntArrayList[] adjacencyList) {
            for (LineImpl line : Sets.union(network.index.getAll(LineImpl.class), network.index.getAll(TieLineImpl.class))) {
                BusExt bus1 = line.getTerminal1().getBusView().getBus();
                BusExt bus2 = line.getTerminal2().getBusView().getBus();
                addToAdjacencyList(bus1, bus2, id2num, adjacencyList);
            }
            for (TwoWindingsTransformerImpl transfo : network.index.getAll(TwoWindingsTransformerImpl.class)) {
                BusExt bus1 = transfo.getTerminal1().getBusView().getBus();
                BusExt bus2 = transfo.getTerminal2().getBusView().getBus();
                addToAdjacencyList(bus1, bus2, id2num, adjacencyList);
            }
            for (ThreeWindingsTransformerImpl transfo : network.index.getAll(ThreeWindingsTransformerImpl.class)) {
                BusExt bus1 = transfo.getLeg1().getTerminal().getBusView().getBus();
                BusExt bus2 = transfo.getLeg2().getTerminal().getBusView().getBus();
                BusExt bus3 = transfo.getLeg3().getTerminal().getBusView().getBus();
                addToAdjacencyList(bus1, bus2, id2num, adjacencyList);
                addToAdjacencyList(bus1, bus3, id2num, adjacencyList);
                addToAdjacencyList(bus2, bus3, id2num, adjacencyList);
            }
        }

        protected abstract C createComponent(int num, int size);

        protected abstract String getComponentLabel();

        protected abstract void setComponentNumber(BusExt bus, int num);

        void update() {
            if (components != null) {
                return;
            }

            long startTime = System.currentTimeMillis();

            // reset
            for (Bus b : network.getBusBreakerView().getBuses()) {
                setComponentNumber((BusExt) b, -1);
            }

            int num = 0;
            Map<String, Integer> id2num = new HashMap<>();
            List<BusExt> num2bus = new ArrayList<>();
            for (Bus bus : network.getBusView().getBuses()) {
                num2bus.add((BusExt) bus);
                id2num.put(bus.getId(), num);
                num++;
            }
            TIntArrayList[] adjacencyList = new TIntArrayList[num];
            for (int i = 0; i < adjacencyList.length; i++) {
                adjacencyList[i] = new TIntArrayList(3);
            }
            fillAdjacencyList(id2num, adjacencyList);

            ConnectedComponentsComputationResult result = GraphUtil.computeConnectedComponents(adjacencyList);

            components = new ArrayList<>(result.getComponentSize().length);
            for (int i = 0; i < result.getComponentSize().length; i++) {
                components.add(createComponent(i, result.getComponentSize()[i]));
            }

            for (int i = 0; i < result.getComponentNumber().length; i++) {
                BusExt bus = num2bus.get(i);
                setComponentNumber(bus, result.getComponentNumber()[i]);
            }

            LOGGER.debug("{} components computed in {} ms", getComponentLabel(), System.currentTimeMillis() - startTime);
        }

        List<C> getConnectedComponents() {
            update();
            return components;
        }

        C getComponent(int num) {
            // update() must not be put here, but explicitly called each time before because update may
            // trigger a new component computation and so on a change in the value of the num component already passed
            // (and outdated consequently) in parameter of this method
            return num != -1 ? components.get(num) : null;
        }

    }

    static final class ConnectedComponentsManager extends AbstractComponentsManager<ConnectedComponentImpl> {

        private ConnectedComponentsManager(NetworkImpl network) {
            super(network);
        }

        @Override
        protected void fillAdjacencyList(Map<String, Integer> id2num, TIntArrayList[] adjacencyList) {
            super.fillAdjacencyList(id2num, adjacencyList);
            for (HvdcLineImpl line : network.index.getAll(HvdcLineImpl.class)) {
                BusExt bus1 = line.getConverterStation1().getTerminal().getBusView().getBus();
                BusExt bus2 = line.getConverterStation2().getTerminal().getBusView().getBus();
                addToAdjacencyList(bus1, bus2, id2num, adjacencyList);
            }
        }

        @Override
        protected String getComponentLabel() {
            return "Connected";
        }

        @Override
        protected void setComponentNumber(BusExt bus, int num) {
            Objects.requireNonNull(bus);
            bus.setConnectedComponentNumber(num);
        }

        protected ConnectedComponentImpl createComponent(int num, int size) {
            return new ConnectedComponentImpl(num, size, network.ref);
        }
    }

    static final class SynchronousComponentsManager extends AbstractComponentsManager<SynchronousComponentImpl> {

        private SynchronousComponentsManager(NetworkImpl network) {
            super(network);
        }

        protected SynchronousComponentImpl createComponent(int num, int size) {
            return new SynchronousComponentImpl(num, size, network.ref);
        }

        @Override
        protected String getComponentLabel() {
            return "Synchronous";
        }

        @Override
        protected void setComponentNumber(BusExt bus, int num) {
            Objects.requireNonNull(bus);
            bus.setSynchronousComponentNumber(num);
        }
    }

    private class VariantImpl implements Variant {

        private final ConnectedComponentsManager connectedComponentsManager
                = new ConnectedComponentsManager(NetworkImpl.this);

        private final SynchronousComponentsManager synchronousComponentsManager
                = new SynchronousComponentsManager(NetworkImpl.this);

        @Override
        public VariantImpl copy() {
            return new VariantImpl();
        }

    }

    private final VariantArray<VariantImpl> variants;

    ConnectedComponentsManager getConnectedComponentsManager() {
        return variants.get().connectedComponentsManager;
    }

    SynchronousComponentsManager getSynchronousComponentsManager() {
        return variants.get().synchronousComponentsManager;
    }

    @Override
    public void extendVariantArraySize(int initVariantArraySize, int number, final int sourceIndex) {
        variants.push(number, () -> variants.copy(sourceIndex));
    }

    @Override
    public void reduceVariantArraySize(int number) {
        variants.pop(number);
    }

    @Override
    public void deleteVariantArrayElement(int index) {
        variants.delete(index);
    }

    @Override
    public void allocateVariantArrayElement(int[] indexes, final int sourceIndex) {
        variants.allocate(indexes, () -> variants.copy(sourceIndex));
    }

    @Override
    protected String getTypeDescription() {
        return "Network";
    }

    @Override
    public void merge(Network other) {
        NetworkImpl otherNetwork = (NetworkImpl) other;

        // this check must not be done on the number of variants but on the size
        // of the internal variant array because the network can have only
        // one variant but an internal array with a size greater that one and
        // some re-usable variants
        if (variantManager.getVariantArraySize() != 1 || otherNetwork.variantManager.getVariantArraySize() != 1) {
            throw new PowsyblException("Merging of multi-variants network is not supported");
        }

        long start = System.currentTimeMillis();

        // check mergeability
        Multimap<Class<? extends Identifiable>, String> intersection = index.intersection(otherNetwork.index);
        for (Map.Entry<Class<? extends Identifiable>, Collection<String>> entry : intersection.asMap().entrySet()) {
            Class<? extends Identifiable> clazz = entry.getKey();
            if (clazz == DanglingLineImpl.class) { // fine for dangling lines
                continue;
            }
            Collection<String> objs = entry.getValue();
            if (!objs.isEmpty()) {
                throw new PowsyblException("The following object(s) of type "
                        + clazz.getSimpleName() + " exist(s) in both networks: "
                        + objs);
            }
        }

        // try to find dangling lines couples
        List<MergedLine> lines = new ArrayList<>();
        for (DanglingLine dl2 : Lists.newArrayList(other.getDanglingLines())) {
            Map<String, DanglingLine> dl1byXnodeCode = new HashMap<>();
            for (DanglingLine dl1 : getDanglingLines()) {
                if (dl1.getUcteXnodeCode() != null) {
                    dl1byXnodeCode.put(dl1.getUcteXnodeCode(), dl1);
                }
            }
            DanglingLine dl1 = getDanglingLineByTheOther(dl2, dl1byXnodeCode);
            mergeDanglingLines(lines, dl1, dl2);
        }

        // do not forget to remove the other network from its index!!!
        otherNetwork.index.remove(otherNetwork);

        // merge the indexes
        index.merge(otherNetwork.index);

        // fix network back reference of the other network objects
        otherNetwork.ref.setRef(ref);

        Multimap<Boundary, MergedLine> mergedLineByBoundary = HashMultimap.create();
        replaceDanglingLineByLine(lines, mergedLineByBoundary);

        if (!lines.isEmpty()) {
            LOGGER.info("{} dangling line couples have been replaced by a line: {}", lines.size(),
                    mergedLineByBoundary.asMap().entrySet().stream().map(e -> e.getKey() + ": " + e.getValue().size()).collect(Collectors.toList()));
        }

        // update the source format
        if (!sourceFormat.equals(otherNetwork.sourceFormat)) {
            sourceFormat = "hybrid";
        }

        LOGGER.info("Merging of {} done in {} ms", id, System.currentTimeMillis() - start);
    }

    private DanglingLine getDanglingLineByTheOther(DanglingLine dl2, Map<String, DanglingLine> dl1byXnodeCode) {
        DanglingLine dl1 = getDanglingLine(dl2.getId());
        if (dl1 == null) {
            // mapping by ucte xnode code
            if (dl2.getUcteXnodeCode() != null) {
                dl1 = dl1byXnodeCode.get(dl2.getUcteXnodeCode());
            }
        } else {
            // mapping by id
            if (dl1.getUcteXnodeCode() != null && dl2.getUcteXnodeCode() != null
                    && !dl1.getUcteXnodeCode().equals(dl2.getUcteXnodeCode())) {
                throw new PowsyblException("Dangling line couple " + dl1.getId()
                        + " have inconsistent Xnodes (" + dl1.getUcteXnodeCode()
                        + "!=" + dl2.getUcteXnodeCode() + ")");
            }
        }
        return dl1;
    }

    private void mergeDanglingLines(List<MergedLine> lines, DanglingLine dl1, DanglingLine dl2) {
        if (dl1 != null) {
            MergedLine l = new MergedLine();
            l.id = dl1.getId().compareTo(dl2.getId()) < 0 ? dl1.getId() + " + " + dl2.getId() : dl2.getId() + " + " + dl1.getId();
            Terminal t1 = dl1.getTerminal();
            Terminal t2 = dl2.getTerminal();
            VoltageLevel vl1 = t1.getVoltageLevel();
            VoltageLevel vl2 = t2.getVoltageLevel();
            l.voltageLevel1 = vl1.getId();
            l.voltageLevel2 = vl2.getId();
            l.xnode = dl1.getUcteXnodeCode();
            l.half1.id = dl1.getId();
            l.half1.name = dl1.getNameOrId();
            l.half1.r = dl1.getR();
            l.half1.x = dl1.getX();
            l.half1.g1 = dl1.getG();
            l.half1.g2 = 0;
            l.half1.b1 = dl1.getB();
            l.half1.b2 = 0;
            l.half1.xnodeP = dl1.getP0();
            l.half1.xnodeQ = dl1.getQ0();
            l.half2.id = dl2.getId();
            l.half2.name = dl2.getNameOrId();
            l.half2.r = dl2.getR();
            l.half2.x = dl2.getX();
            l.half2.g2 = dl2.getG();
            l.half2.g1 = 0;
            l.half2.b2 = dl2.getB();
            l.half2.b1 = 0;
            l.half2.xnodeP = dl2.getP0();
            l.half2.xnodeQ = dl2.getQ0();
            l.limits1 = dl1.getCurrentLimits();
            l.limits2 = dl2.getCurrentLimits();
            if (t1.getVoltageLevel().getTopologyKind() == TopologyKind.BUS_BREAKER) {
                Bus b1 = t1.getBusBreakerView().getBus();
                if (b1 != null) {
                    l.bus1 = b1.getId();
                }
                l.connectableBus1 = t1.getBusBreakerView().getConnectableBus().getId();
            } else {
                l.node1 = t1.getNodeBreakerView().getNode();
            }
            if (t2.getVoltageLevel().getTopologyKind() == TopologyKind.BUS_BREAKER) {
                Bus b2 = t2.getBusBreakerView().getBus();
                if (b2 != null) {
                    l.bus2 = b2.getId();
                }
                l.connectableBus2 = t2.getBusBreakerView().getConnectableBus().getId();
            } else {
                l.node2 = t2.getNodeBreakerView().getNode();
            }
            l.p1 = t1.getP();
            l.q1 = t1.getQ();
            l.p2 = t2.getP();
            l.q2 = t2.getQ();
            l.country1 = vl1.getSubstation().getCountry().orElse(null);
            l.country2 = vl2.getSubstation().getCountry().orElse(null);
            mergeProperties(dl1, dl2, l.properties);
            lines.add(l);

            // remove the 2 dangling lines
            dl1.remove();
            dl2.remove();
        }
    }

    private void mergeProperties(DanglingLine dl1, DanglingLine dl2, Properties properties) {
        Set<String> dl1Properties = dl1.getPropertyNames();
        Set<String> dl2Properties = dl2.getPropertyNames();
        Set<String> commonProperties = Sets.intersection(dl1Properties, dl2Properties);
        Sets.difference(dl1Properties, commonProperties).forEach(prop -> properties.setProperty(prop, dl1.getProperty(prop)));
        Sets.difference(dl2Properties, commonProperties).forEach(prop -> properties.setProperty(prop, dl2.getProperty(prop)));
        commonProperties.forEach(prop -> {
            if (dl1.getProperty(prop).equals(dl2.getProperty(prop))) {
                properties.setProperty(prop, dl1.getProperty(prop));
            } else if (dl1.getProperty(prop).isEmpty()) {
                LOGGER.debug("Inconsistencies of property '{}' between both sides of merged line. Side 1 is empty, keeping side 2 value '{}'", prop, dl2.getProperty(prop));
                properties.setProperty(prop, dl2.getProperty(prop));
            } else if (dl2.getProperty(prop).isEmpty()) {
                LOGGER.debug("Inconsistencies of property '{}' between both sides of merged line. Side 2 is empty, keeping side 1 value '{}'", prop, dl1.getProperty(prop));
                properties.setProperty(prop, dl1.getProperty(prop));
            } else {
                LOGGER.debug("Inconsistencies of property '{}' between both sides of merged line. '{}' on side 1 and '{}' on side 2. Removing the property of merged line", prop, dl1.getProperty(prop), dl2.getProperty(prop));
            }
        });
        dl1Properties.forEach(prop -> properties.setProperty(prop + "_1", dl1.getProperty(prop)));
        dl2Properties.forEach(prop -> properties.setProperty(prop + "_2", dl2.getProperty(prop)));
    }

    private void replaceDanglingLineByLine(List<MergedLine> lines, Multimap<Boundary, MergedLine> mergedLineByBoundary) {
        for (MergedLine mergedLine : lines) {
            LOGGER.debug("Replacing dangling line couple '{}' (xnode={}, country1={}, country2={}) by a line",
                    mergedLine.id, mergedLine.xnode, mergedLine.country1, mergedLine.country2);
            TieLineAdderImpl la = newTieLine()
                    .setId(mergedLine.id)
                    .setName(mergedLine.half1.name + " + " + mergedLine.half2.name)
                    .setVoltageLevel1(mergedLine.voltageLevel1)
                    .setVoltageLevel2(mergedLine.voltageLevel2)
                    .line1().setId(mergedLine.half1.id)
                        .setName(mergedLine.half1.name)
                        .setR(mergedLine.half1.r)
                        .setX(mergedLine.half1.x)
                        .setG1(mergedLine.half1.g1)
                        .setG2(mergedLine.half1.g2)
                        .setB1(mergedLine.half1.b1)
                        .setB2(mergedLine.half1.b2)
                        .setXnodeP(mergedLine.half1.xnodeP)
                        .setXnodeQ(mergedLine.half1.xnodeQ)
                    .line2().setId(mergedLine.half2.id)
                        .setName(mergedLine.half2.name)
                        .setR(mergedLine.half2.r)
                        .setX(mergedLine.half2.x)
                        .setG1(mergedLine.half2.g1)
                        .setG2(mergedLine.half2.g2)
                        .setB1(mergedLine.half2.b1)
                        .setB2(mergedLine.half2.b2)
                        .setXnodeP(mergedLine.half2.xnodeP)
                        .setXnodeQ(mergedLine.half2.xnodeQ)
                    .setUcteXnodeCode(mergedLine.xnode);
            if (mergedLine.bus1 != null) {
                la.setBus1(mergedLine.bus1);
            }
            la.setConnectableBus1(mergedLine.connectableBus1);
            if (mergedLine.bus2 != null) {
                la.setBus2(mergedLine.bus2);
            }
            la.setConnectableBus2(mergedLine.connectableBus2);
            if (mergedLine.node1 != null) {
                la.setNode1(mergedLine.node1);
            }
            if (mergedLine.node2 != null) {
                la.setNode2(mergedLine.node2);
            }
            TieLineImpl l = la.add();
            l.setCurrentLimits(Side.ONE, (CurrentLimitsImpl) mergedLine.limits1);
            l.setCurrentLimits(Side.TWO, (CurrentLimitsImpl) mergedLine.limits2);
            l.getTerminal1().setP(mergedLine.p1).setQ(mergedLine.q1);
            l.getTerminal2().setP(mergedLine.p2).setQ(mergedLine.q2);
            mergedLine.properties.forEach((key, val) -> l.setProperty(key.toString(), val.toString()));

            mergedLineByBoundary.put(new Boundary(mergedLine.country1, mergedLine.country2), mergedLine);
        }
    }

    class MergedLine {
        String id;
        String voltageLevel1;
        String voltageLevel2;
        String xnode;
        String bus1;
        String bus2;
        String connectableBus1;
        String connectableBus2;
        Integer node1;
        Integer node2;
        Properties properties = new Properties();

        class HalfMergedLine {
            String id;
            String name;
            double r;
            double x;
            double g1;
            double g2;
            double b1;
            double b2;
            double xnodeP;
            double xnodeQ;
        }

        final HalfMergedLine half1 = new HalfMergedLine();
        final HalfMergedLine half2 = new HalfMergedLine();

        CurrentLimits limits1;
        CurrentLimits limits2;
        double p1;
        double q1;
        double p2;
        double q2;

        Country country1;
        Country country2;
    }

    @Override
    public void merge(Network... others) {
        for (Network other : others) {
            merge(other);
        }
    }

    @Override
    public void addListener(NetworkListener listener) {
        listeners.add(listener);
    }

    @Override
    public void removeListener(NetworkListener listener) {
        listeners.remove(listener);
    }
}