/**
 * Copyright (c) 2017-2018, RTE (http://www.rte-france.com)
 * 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.cgmes.conversion.elements;

import java.util.List;

import org.apache.commons.math3.complex.Complex;

import com.powsybl.cgmes.conversion.Context;
import com.powsybl.cgmes.model.CgmesNames;
import com.powsybl.cgmes.model.PowerFlow;
import com.powsybl.iidm.network.DanglingLine;
import com.powsybl.iidm.network.DanglingLineAdder;
import com.powsybl.iidm.network.Line;
import com.powsybl.iidm.network.LineAdder;
import com.powsybl.iidm.network.TieLineAdder;
import com.powsybl.iidm.network.util.SV;
import com.powsybl.triplestore.api.PropertyBag;

/**
 * @author Luma ZamarreƱo <zamarrenolm at aia.es>
 */
public class ACLineSegmentConversion extends AbstractBranchConversion {

    public ACLineSegmentConversion(PropertyBag line, Context context) {
        super(CgmesNames.AC_LINE_SEGMENT, line, context);
    }

    @Override
    public boolean valid() {
        // An AC line segment end voltage level may be null
        // (when it is in the boundary and the boundary nodes are not converted)
        // So we do not use the generic validity check for conducting equipment
        // or branch. We only ensure we have nodes at both ends
        for (int k = 1; k <= 2; k++) {
            if (nodeId(k) == null) {
                missing(nodeIdPropertyName() + k);
                return false;
            }
        }
        return true;
    }

    public String boundaryNode() {
        // Only one of the end points can be in the boundary
        if (isBoundary(1)) {
            return nodeId(1);
        } else if (isBoundary(2)) {
            return nodeId(2);
        }
        return null;
    }

    @Override
    public void convert() {
        if (isBoundary(1)) {
            convertLineOnBoundary(1);
        } else if (isBoundary(2)) {
            convertLineOnBoundary(2);
        } else {
            convertLine();
        }
    }

    private void convertLineOnBoundary(int boundarySide) {
        String boundaryNode = nodeId(boundarySide);
        List<PropertyBag> lines = context.boundary().linesAtNode(boundaryNode);

        // If we have created buses and substations for boundary nodes,
        // convert as regular lines both lines at boundary node
        if (context.config().convertBoundary()) {
            // Convert this line
            convertLine();
            if (lines.size() == 2) {
                // Convert the other line
                PropertyBag other = lines.get(0).getId(CgmesNames.AC_LINE_SEGMENT).equals(id)
                        ? lines.get(1)
                        : lines.get(0);
                new ACLineSegmentConversion(other, context).convertLine();
            }
        } else {
            if (lines.size() == 2) {
                convertMergedLinesAtNode(lines, boundaryNode);
            } else {
                convertDanglingLine(boundarySide);
            }
        }
    }

    private void convertLine() {
        double r = p.asDouble("r");
        double x = p.asDouble("x");
        double bch = p.asDouble("bch");
        double gch = p.asDouble("gch", 0.0);
        final LineAdder adder = context.network().newLine()
                .setEnsureIdUnicity(false)
                .setR(r)
                .setX(x)
                .setG1(gch / 2)
                .setG2(gch / 2)
                .setB1(bch / 2)
                .setB2(bch / 2);
        identify(adder);
        connect(adder);
        final Line l = adder.add();
        convertedTerminals(l.getTerminal1(), l.getTerminal2());
    }

    private void convertDanglingLine(int boundarySide) {
        // Non-boundary side (other side) of the line
        int modelSide = 3 - boundarySide;
        String boundaryNode = nodeId(boundarySide);

        // check again boundary node is correct
        assert isBoundary(boundarySide) && !isBoundary(modelSide);

        PowerFlow f = new PowerFlow(0, 0);
        // Only consider potential power flow at boundary side if that side is connected
        if (terminalConnected(boundarySide) && context.boundary().hasPowerFlow(boundaryNode)) {
            f = context.boundary().powerFlowAtNode(boundaryNode);
        }
        // There should be some equipment at boundarySide to model exchange through that
        // point
        // But we have observed, for the test case conformity/miniBusBranch,
        // that the ACLineSegment:
        // _5150a037-e241-421f-98b2-fe60e5c90303 XQ1-N1
        // ends in a boundary node where there is no other line,
        // does not have energy consumer or equivalent injection
        if (terminalConnected(boundarySide) && !context.boundary().hasPowerFlow(boundaryNode)) {
            missing("Equipment for modeling consumption/injection at boundary node");
        }

        double r = p.asDouble("r");
        double x = p.asDouble("x");
        double bch = p.asDouble("bch");
        double gch = p.asDouble("gch", 0.0);
        DanglingLineAdder adder = voltageLevel(modelSide).newDanglingLine()
                .setEnsureIdUnicity(false)
                .setR(r)
                .setX(x)
                .setG(gch)
                .setB(bch)
                .setUcteXnodeCode(findUcteXnodeCode(boundaryNode))
                .setP0(f.p())
                .setQ0(f.q());
        identify(adder);
        connect(adder, modelSide);
        DanglingLine dl = adder.add();
        context.convertedTerminal(terminalId(modelSide), dl.getTerminal(), 1, powerFlow(modelSide));

        // If we do not have power flow at model side and we can compute it,
        // do it and assign the result at the terminal of the dangling line
        if (context.config().computeFlowsAtBoundaryDanglingLines()
                && terminalConnected(modelSide)
                && !powerFlow(modelSide).defined()
                && context.boundary().hasVoltage(boundaryNode)) {
            double v = context.boundary().vAtBoundary(boundaryNode);
            double angle = context.boundary().angleAtBoundary(boundaryNode);
            // The net sum of power flow "entering" at boundary is "exiting"
            // through the line, we have to change the sign of the sum of flows
            // at the node when we consider flow at line end
            SV svboundary = new SV(-f.p(), -f.q(), v, angle);
            // The other side power flow must be computed taking into account
            // the same criteria used for ACLineSegment: total shunt admittance
            // is divided in 2 equal shunt admittance at each side of series impedance
            double g = dl.getG() / 2;
            double b = dl.getB() / 2;
            SV svmodel = svboundary.otherSide(dl.getR(), dl.getX(), g, b, g, b, 1);
            dl.getTerminal().setP(svmodel.getP());
            dl.getTerminal().setQ(svmodel.getQ());
        }
    }

    private String findUcteXnodeCode(String boundaryNode) {
        return context.boundary().nameAtBoundary(boundaryNode);
    }

    private void convertMergedLinesAtNode(List<PropertyBag> lines, String boundaryNode) {
        PropertyBag other = lines.get(0).getId(CgmesNames.AC_LINE_SEGMENT).equals(id)
                ? lines.get(1)
                : lines.get(0);
        String otherId = other.getId(CgmesNames.AC_LINE_SEGMENT);
        String otherName = other.getId("name");
        ACLineSegmentConversion otherc = new ACLineSegmentConversion(other, context);

        // Boundary node is common to both lines,
        // identify the end that will be preserved for this line and the other line
        int thisEnd = 1;
        if (nodeId(1).equals(boundaryNode)) {
            thisEnd = 2;
        }
        int otherEnd = 1;
        if (otherc.nodeId(1).equals(boundaryNode)) {
            otherEnd = 2;
        }

        String iidmVoltageLevelId1 = iidmVoltageLevelId(thisEnd);
        String iidmVoltageLevelId2 = otherc.iidmVoltageLevelId(otherEnd);
        boolean mt1connected = terminalConnected(thisEnd);
        boolean mt2connected = otherc.terminalConnected(otherEnd);
        String mbus1 = busId(thisEnd);
        String mbus2 = otherc.busId(otherEnd);
        int mnode1 = -1;
        int mnode2 = -1;
        if (context.nodeBreaker()) {
            mnode1 = iidmNode(thisEnd);
            mnode2 = otherc.iidmNode(otherEnd);
        }

        double lineR = p.asDouble("r");
        double lineX = p.asDouble("x");
        double lineGch = p.asDouble("gch", 0);
        double lineBch = p.asDouble("bch", 0);
        double otherR = other.asDouble("r");
        double otherX = other.asDouble("x");
        double otherGch = other.asDouble("gch", 0);
        double otherBch = other.asDouble("bch", 0);

        String id1 = context.namingStrategy().getId("Line", id);
        String id2 = context.namingStrategy().getId("Line", otherId);
        String name1 = context.namingStrategy().getName("Line", name);
        String name2 = context.namingStrategy().getName("Line", otherName);

        Line mline;
        if (context.config().mergeLinesUsingQuadripole()) {
            PiModel pi1 = new PiModel();
            pi1.r = lineR;
            pi1.x = lineX;
            pi1.g1 = lineGch / 2.0;
            pi1.b1 = lineBch / 2.0;
            pi1.g2 = pi1.g1;
            pi1.b2 = pi1.b1;
            PiModel pi2 = new PiModel();
            pi2.r = otherR;
            pi2.x = otherX;
            pi2.g1 = otherGch / 2.0;
            pi2.b1 = otherBch / 2.0;
            pi2.g2 = pi2.g1;
            pi2.b2 = pi2.b1;
            PiModel pim = Quadripole.from(pi1).cascade(Quadripole.from(pi2)).toPiModel();
            LineAdder adder = context.network().newLine()
                    .setR(pim.r)
                    .setX(pim.x)
                    .setG1(pim.g1)
                    .setG2(pim.g2)
                    .setB1(pim.b1)
                    .setB2(pim.b2);
            identify(adder, id1 + " + " + id2, name1 + " + " + name2);
            connect(adder, iidmVoltageLevelId1, mbus1, mt1connected, mnode1, iidmVoltageLevelId2, mbus2, mt2connected, mnode2);
            mline = adder.add();
        } else {
            TieLineAdder adder = context.network().newTieLine()
                    .line1()
                    .setId(id1)
                    .setName(name1)
                    .setR(lineR)
                    .setX(lineX)
                    .setG1(lineGch / 2)
                    .setG2(lineGch / 2)
                    .setB1(lineBch / 2)
                    .setB2(lineBch / 2)
                    .setXnodeP(0)
                    .setXnodeQ(0)
                    .line2()
                    .setId(id2)
                    .setName(name2)
                    .setR(otherR)
                    .setX(otherX)
                    .setG1(otherGch / 2)
                    .setG2(otherGch / 2)
                    .setB1(otherBch / 2)
                    .setB2(otherBch / 2)
                    .setXnodeP(0)
                    .setXnodeQ(0)
                    .setUcteXnodeCode(findUcteXnodeCode(boundaryNode));
            identify(adder, id1 + " + " + id2, name1 + " + " + name2);
            connect(adder, iidmVoltageLevelId1, mbus1, mt1connected, mnode1, iidmVoltageLevelId2, mbus2, mt2connected, mnode2);
            mline = adder.add();
        }
        context.convertedTerminal(terminalId(thisEnd), mline.getTerminal1(), 1, powerFlow(thisEnd));
        context.convertedTerminal(otherc.terminalId(otherEnd), mline.getTerminal2(), 2, otherc.powerFlow(otherEnd));
    }

    static class PiModel {
        double r;
        double x;
        double g1;
        double b1;
        double g2;
        double b2;
    }

    static class Quadripole {
        Complex a;
        Complex b;
        Complex c;
        Complex d;

        public static Quadripole from(PiModel pi) {
            Quadripole y1 = Quadripole.fromShuntAdmittance(pi.g1, pi.b1);
            Quadripole z = Quadripole.fromSeriesImpedance(pi.r, pi.x);
            Quadripole y2 = Quadripole.fromShuntAdmittance(pi.g2, pi.b2);
            return y1.cascade(z).cascade(y2);
        }

        public static Quadripole fromSeriesImpedance(double r, double x) {
            Quadripole q = new Quadripole();
            q.a = new Complex(1);
            q.b = new Complex(r, x);
            q.c = new Complex(0);
            q.d = new Complex(1);
            return q;
        }

        public static Quadripole fromShuntAdmittance(double g, double b) {
            Quadripole q = new Quadripole();
            q.a = new Complex(1);
            q.b = new Complex(0);
            q.c = new Complex(g, b);
            q.d = new Complex(1);
            return q;
        }

        public Quadripole cascade(Quadripole q2) {
            Quadripole q1 = this;
            Quadripole qr = new Quadripole();
            qr.a = q1.a.multiply(q2.a).add(q1.b.multiply(q2.c));
            qr.b = q1.a.multiply(q2.b).add(q1.b.multiply(q2.d));
            qr.c = q1.c.multiply(q2.a).add(q1.d.multiply(q2.c));
            qr.d = q1.c.multiply(q2.b).add(q1.d.multiply(q2.d));
            return qr;
        }

        public PiModel toPiModel() {
            PiModel pi = new PiModel();

            // Y2 = (A - 1)/B
            // Y1 = (D - 1)/B
            Complex y1 = d.add(-1).divide(b);
            Complex y2 = a.add(-1).divide(b);

            pi.r = b.getReal();
            pi.x = b.getImaginary();
            pi.g1 = y1.getReal();
            pi.b1 = y1.getImaginary();
            pi.g2 = y2.getReal();
            pi.b2 = y2.getImaginary();
            return pi;
        }
    }
}