package com.henrypump.poc;


import de.vandermeer.asciitable.v2.RenderedTable;
import de.vandermeer.asciitable.v2.V2_AsciiTable;
import de.vandermeer.asciitable.v2.render.V2_AsciiTableRenderer;
import de.vandermeer.asciitable.v2.render.WidthAbsoluteEven;
import de.vandermeer.asciitable.v2.themes.V2_E_TableThemes;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;

import static java.lang.Math.exp;
import static java.lang.Math.pow;
import static java.lang.Math.sqrt;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.time.LocalDate;

/**
 * Created by patrickjmcd on 1/31/17.
 */
public class Well {
    private String wellName;
    protected Simulation sim;
    protected Database db;
    protected WellTest wellTest;

    /* IO */
    AnalogIn inclinometer;
    AnalogIn loadCell;
    DigitalOut runCommand;
    private double currentSurfacePosition;
    private double currentSurfaceLoad;
    private double currentDownholePosition;
    private double currentDownholeLoad;

    /* CARDS */
    private Card currentCard;
    private Card[] cardStorage = new Card[100];

    // CONSTANTS
    private static double YM_STEEL = 30.5;
    private static double YM_FIBERGLASS = 7.2;
    public static final int BAD_STATUS = 0;
    public static final int GOOD_STATUS = 1;

    /* USER INPUTS */
    private double dt;
    private double tubingHeadPressure;
    private double fluidGradient;
    private double sbfriction;
    private int numTapers;
    private double tubingAnchorDepth;
    private double pumpDiameter;
    private double tubingID, tubingOD;
    private double structuralRating;
    private double[] c = new double[11];
    private double[] rodLength = new double[11];
    private double[] rodDiameter = new double[11];
    private double[] rodYM = new double[11];
    private double[] rodWeightPerFoot = new double[11];


    // CALCULATED TAPER PARAMETERS
    private int nT1; // should always be equal to number of tapers + 1
    private double frictionEstimate;
    private double theoreticalMaxFluidLoad;
    private double[] a = new double[11];
    private double[] area = new double[11];
    private double[] pressure = new double[11];
    private double[] buoyantForce = new double[11];
    private double buoyantForceTotal;
    private double[] stretch = new double[11];
    private double[] weightData = new double[11];
    private double weightDataTotal;
    private double[] annularForceData = new double[11];
    private double annularForceDataTotal;
    private double[] force = new double[11];
    private double[] alpha = new double[11];
    private double[] xOverA = new double[11];
    private double[] factor = new double[11];
    private int[] lagIndex = new int[11];
    private int[] lengthRequired = new int[11];
    private int[] centerPoint = new int[11];
    private double[] rodDepth = new double[11];
    private double rodDepthTotal;
    private double[] rodWeightAir = new double[11];
    private double rodWeightAirTotal;
    private double[] rodWeightFluid = new double[11];
    private double rodWeightFluidTotal;
    private double pumpArea;
    private double tubingCrossSectionalArea;

    // Statuses
    private int runStatus;
    public static final int RUNSTATUS_STOPPED = 0;
    public static final int RUNSTATUS_STARTING = 1;
    public static final int RUNSTATUS_RUNNING = 2;
    public static final int RUNSTATUS_PUMPEDOFF = 3;
    public static final int RUNSTATUS_FAULTED = 4;
    public static final int RUNSTATUS_LOCKEDOUT = 5;

    private boolean permissiveOK;
    private long strokesSinceStart = 0;
    private long startupStrokes = 1;
    private long strokesToday = 0;
    private long strokesLifetime;
    private int pointCounter =0;

    // DIRECTION
    private static final int DIRECTION_UNKNOWN = 0;
    private static final int DIRECTION_UP = 1;
    private static final int DIRECTION_DOWN = 2;
    private int direction = DIRECTION_UNKNOWN;
    private int lastDirection = DIRECTION_UNKNOWN;

    // Modes
    private int runMode;
    public static final int RUNMODE_POC = 0;
    public static final int RUNMODE_MANUAL = 1;
    public static final int RUNMODE_TIMER = 2;


    // Intermediate Variables
    private double[][] topPosArray = new double[10][100];
    private double[][] topLoadArray = new double[10][100];
    private double loadBefore = 0.0;
    private double loadAfter = 0.0;
    private double loadBefore3 = 0.0;
    private double loadAfter3 = 0.0;
    private int[] count = new int[11];
    private double sPositionPrevious;

    // Fluid Makeup
    private double fluidOilRatio; // BBL of oil per 1 BBL fluid
    private double fluidWaterRatio; // BBL of water per 1 BBL fluid
    private double fluidGasRatio; // MCF of gas per 1 BBL fluid

    // DATE & TIME PARAMETERS
    private LocalDate lastCheckedDate = null;

    private boolean isNewDay(){
        LocalDate today = LocalDate.now();
        boolean ret = lastCheckedDate == null || today.isAfter(lastCheckedDate);
        lastCheckedDate = today;
        return ret;
    }

    // Measurements
    private Measurement strokeSpeed;
    private Measurement downholeGrossStroke;
    private Measurement downholeNetStroke;
    private Measurement fluidLevel;
    private Measurement fluidLoad;
    private Measurement inflowRate;
    private Measurement peakPolishedRodLoad;
    private Measurement minPolishedRodLoad;
    private Measurement percentRun;
    private Measurement polishedRodHP;
    private Measurement pumpHP;
    private Measurement fluidProduced;
    private Measurement oilProduced;
    private Measurement waterProduced;
    private Measurement gasProduced;
    private Measurement pumpIntakePressure;
    private Measurement surfaceStrokeLength;
    private Measurement tubingMovement;

    Well(String wellName, int inclinometerChannel, int loadCellChannel, int runCommandChannel){
        this.wellName = wellName;
        db = new Database();
        strokesLifetime = db.getLastStrokeNum() + 1;
        currentCard = new Card(strokesLifetime);
        inclinometer = new AnalogIn(inclinometerChannel, 0, 100, 0, 100);
        loadCell = new AnalogIn(loadCellChannel, 0, 50000, 0, 50000);
        runCommand  = new DigitalOut(runCommandChannel, 0);

        strokeSpeed = new Measurement("Stroke Speed", true, db, 0.5, 600);
        downholeGrossStroke = new Measurement("Downhole Gross Stroke", true, db, 0.5, 600);
        downholeNetStroke = new Measurement("Downhole Net Stroke", true, db, 0.5, 600);
        fluidLevel = new Measurement("Fluid Level", true, db, 10, 600);
        fluidLoad = new Measurement("Fluid Load", true, db, 20, 600);
        inflowRate = new Measurement("Inflow Rate", true, db, 0.5, 600);
        peakPolishedRodLoad = new Measurement("Peak PRL", true, db, 50, 600);
        minPolishedRodLoad = new Measurement("Min PRL", true, db, 50, 600);
        percentRun = new Measurement("Percent Run", true, db, 1.0, 600);
        polishedRodHP = new Measurement("Polished Rod HP", true, db, 0.25, 600);
        pumpHP = new Measurement("Pump HP", true, db, 0.25, 600);
        fluidProduced = new Measurement("Fluid Produced", true, db, 1.0, 600);
        oilProduced = new Measurement("Oil Produced", true, db, 1.0, 600);
        waterProduced = new Measurement("Water Produced", true, db, 1.0, 600);
        gasProduced = new Measurement("Gas Produced", true, db, 1.0, 600);
        pumpIntakePressure = new Measurement("Pump Intake Pressure", true, db, 5.0, 600);
        surfaceStrokeLength = new Measurement("Surface Stroke", true, db, 0.5, 1800);
        tubingMovement = new Measurement("Tubing Movement", true, db, 0.5, 600);
    }

    Well(String wellName, String simFileName, int inclinometerChannel, int loadCellChannel, int runCommandChannel){
        this.wellName = wellName;
        sim = new Simulation(simFileName);
        db = new Database();
        strokesLifetime = db.getLastStrokeNum() + 1;
        currentCard = new Card(strokesLifetime);
        inclinometer = new AnalogIn(inclinometerChannel, 0, 100, 0, 100);
        loadCell = new AnalogIn(loadCellChannel, 0, 50000, 0, 50000);
        runCommand  = new DigitalOut(runCommandChannel, 0);

        strokeSpeed = new Measurement("Stroke Speed", true, db, 0.5, 600);
        downholeGrossStroke = new Measurement("Downhole Gross Stroke", true, db, 0.5, 600);
        downholeNetStroke = new Measurement("Downhole Net Stroke", true, db, 0.5, 600);
        fluidLevel = new Measurement("Fluid Level", true, db, 10, 600);
        fluidLoad = new Measurement("Fluid Load", true, db, 20, 600);
        inflowRate = new Measurement("Inflow Rate", true, db, 0.5, 600);
        peakPolishedRodLoad = new Measurement("Peak PRL", true, db, 50, 600);
        minPolishedRodLoad = new Measurement("Min PRL", true, db, 50, 600);
        percentRun = new Measurement("Percent Run", true, db, 1.0, 600);
        polishedRodHP = new Measurement("Polished Rod HP", true, db, 0.25, 600);
        pumpHP = new Measurement("Pump HP", true, db, 0.25, 600);
        fluidProduced = new Measurement("Fluid Produced", true, db, 1.0, 600);
        oilProduced = new Measurement("Oil Produced", true, db, 1.0, 600);
        waterProduced = new Measurement("Water Produced", true, db, 1.0, 600);
        gasProduced = new Measurement("Gas Produced", true, db, 1.0, 600);
        pumpIntakePressure = new Measurement("Pump Intake Pressure", true, db, 5.0, 600);
        surfaceStrokeLength = new Measurement("Surface Stroke", true, db, 0.5, 1800);
        tubingMovement = new Measurement("Tubing Movement", true, db, 0.5, 600);
    }

    public double getDt() {
        return dt;
    }

    public void setDt(double dt) {
        this.dt = dt;
    }

    public double getTubingHeadPressure() {
        return tubingHeadPressure;
    }

    public void setTubingHeadPressure(double tubingHeadPressure) {
        this.tubingHeadPressure = tubingHeadPressure;
    }

    public double getFluidGradient() {
        return fluidGradient;
    }

    public void setFluidGradient(double fluidGradient) {
        this.fluidGradient = fluidGradient;
    }

    public double getSbfriction() {
        return sbfriction;
    }

    public void setSbfriction(double sbfriction) {
        this.sbfriction = sbfriction;
    }

    public int getNumTapers() {
        return numTapers;
    }

    public void setNumTapers(int numTapers) {
        this.numTapers = numTapers;
    }

    public double getTubingAnchorDepth() {
        return tubingAnchorDepth;
    }

    public void setTubingAnchorDepth(double tubingAnchorDepth) {
        this.tubingAnchorDepth = tubingAnchorDepth;
    }

    public double getPumpDiameter() {
        return pumpDiameter;
    }

    public void setPumpDiameter(double pumpDiameter) {
        this.pumpDiameter = pumpDiameter;
        this.pumpArea = pow(pumpDiameter, 2)  * Math.PI;
    }

    public double getTubingID() {
        return tubingID;
    }

    public void setTubingID(double tubingID) {
        this.tubingID = tubingID;
        this.tubingCrossSectionalArea = (Math.PI / 4) * (pow(this.tubingOD, 2) - pow(this.tubingID,2));
    }

    public double getTubingOD() {
        return tubingOD;
    }

    public void setTubingOD(double tubingOD) {
        this.tubingOD = tubingOD;
        this.tubingCrossSectionalArea = (Math.PI / 4) * (pow(this.tubingOD, 2) - pow(tubingID,2));

    }

    public void setDampingFactor(int i, double c) {
        this.c[i] = c;
    }

    public double getDampingFactor(int i){ return c[i]; }

    public void setRodLength(int i, double rodLength) {
        this.rodLength[i] = rodLength;
    }

    public double getRodLength(int i){ return rodLength[i]; }

    public void setRodDiameter(int i, double rodDiameter) {
        this.rodDiameter[i] = rodDiameter;
    }

    public double getRodDiameter(int i){ return rodDiameter[i]; }

    public void setRodYM(int i, String material) {

        if (material.toLowerCase().equals("steel")) rodYM[i] = YM_STEEL;
        else if (material.toLowerCase().equals("fiberglass")) rodYM[i] = YM_FIBERGLASS;

    }

    public double getRodYM(int i){ return rodYM[i]; }

    public String getRodMaterial(int i){
        if(rodYM[i] == YM_STEEL)
            return "steel";
        if(rodYM[i] == YM_FIBERGLASS)
            return "fiberglass";
        return "unknown";
    }

    public double getFrictionEstimate() {
        return frictionEstimate;
    }

    public double getRodDepthTotal() {
        return rodDepthTotal;
    }

    public double getPumpArea() {
        return pumpArea;
    }

    public double getTubingCrossSectionalArea() {
        return tubingCrossSectionalArea;
    }

    public double getStructuralRating() {
        return structuralRating;
    }

    public void setStructuralRating(double structuralRating) {
        this.structuralRating = structuralRating;
    }

    public String getWellName() {
        return wellName;
    }

    public boolean isPermissiveOK() {
        return permissiveOK;
    }

    public int getRunStatus() {
        return runStatus;
    }

    public int getRunMode() {
        return runMode;
    }

    public double getCurrentSurfacePosition() {
        return currentSurfacePosition;
    }

    public double getCurrentSurfaceLoad() {
        return currentSurfaceLoad;
    }

    public double getCurrentDownholePosition() {
        return currentDownholePosition;
    }

    public double getCurrentDownholeLoad() {
        return currentDownholeLoad;
    }

    public long getStartupStrokes() {
        return startupStrokes;
    }

    public void setStartupStrokes(long startupStrokes) {
        this.startupStrokes = startupStrokes;
    }

    public long getStrokesSinceStart() {
        return strokesSinceStart;
    }

    public long getStrokesToday() {
        return strokesToday;
    }

    public long getStrokesLifetime() {
        return strokesLifetime;
    }

    public int getDirection() {
        return direction;
    }

    public double getFluidOilRatio() {
        return fluidOilRatio;
    }

    public double getFluidWaterRatio() {
        return fluidWaterRatio;
    }

    public double getFluidGasRatio() {
        return fluidGasRatio;
    }

    public void setupFluidRatio(double oilRatio, double waterRatio, double gasRatio){
        fluidOilRatio = oilRatio;
        fluidWaterRatio = waterRatio;
        fluidGasRatio = gasRatio;
    }

    // WELL COMMAND FUNCTIONS
    public void start(String initiator){
        if (runStatus == RUNSTATUS_STOPPED && permissiveOK){
            System.out.println("Starting " + wellName + " from " + initiator + "...");
            runStatus = RUNSTATUS_STARTING;
            strokesSinceStart = 0;
        }
    }

    public void stop(String initiator){
        if (runStatus == RUNSTATUS_STARTING  || runStatus == RUNSTATUS_RUNNING){
            System.out.println("Stopping " + wellName + " from " + initiator + "...");
            runStatus = RUNSTATUS_STOPPED;
        }
    }

    public void checkSafeties(){
        permissiveOK = true;
    }

    // WELL CALCULATION FUNCTIONS

    public static double lookupRodWeightPerFoot(double i_ym, double i_diam) {
        double wtPerFt;
        if (i_ym == YM_STEEL) {
            if (i_diam <= 2 && i_diam > 1.75) {
                wtPerFt = 10.7;
            } else if (i_diam <= 1.75 && i_diam > 1.65) {
                wtPerFt = 8.2;
            } else if (i_diam <= 1.65 && i_diam > 1.5) {
                wtPerFt = 7;
            } else if (i_diam <= 1.5 && i_diam > 1.375) {
                wtPerFt = 6;
            } else if (i_diam <= 1.375 && i_diam > 1.125) {
                wtPerFt = 5;
            } else if (i_diam <= 1.125 && i_diam > 1) {
                wtPerFt = 3.676;
            } else if (i_diam <= 1 && i_diam > 0.875) {
                wtPerFt = 2.904;
            } else if (i_diam <= 0.875 && i_diam > 0.75) {
                wtPerFt = 2.224;
            } else if (i_diam <= 0.75 && i_diam > 0.625) {
                wtPerFt = 1.634;
            } else if (i_diam <= 0.625 && i_diam > 0.5) {
                wtPerFt = 1.13;
            } else if (i_diam <= 0.5) {
                wtPerFt = 0.72;
            } else {
                wtPerFt = 0;
            }
        } else if (i_ym == YM_FIBERGLASS) {
            if (i_diam <= 1.25 && i_diam > 1.125) {
                wtPerFt = 1.2879;
            } else if (i_diam <= 1.125 && i_diam > 1) {
                wtPerFt = 1.09;
            } else if (i_diam <= 1 && i_diam > 0.875) {
                wtPerFt = 0.8188;
            } else if (i_diam <= 0.875 && i_diam > 0.75) {
                wtPerFt = 0.6108;
            } else if (i_diam <= 0.75) {
                wtPerFt = 0.484;
            } else {
                wtPerFt = 0;
            }
        } else {
            wtPerFt = 0;
        }
        return wtPerFt;
    };

    public void parseJSONFile(String jsonFilename){
        JSONParser parser = new JSONParser();
        try {
            Object obj = parser.parse(new FileReader(jsonFilename));
            JSONObject well = (JSONObject) obj;

            Object newDeltaT = well.get("deltaT");
            if (newDeltaT != null) dt = (Double) newDeltaT;

            Object newPumpDiameter = well.get("pumpDiameter");
            if (newPumpDiameter != null) setPumpDiameter((Double) newPumpDiameter);

            Object newFluidGradient = well.get("fluidGradient");
            if (newFluidGradient != null) fluidGradient = (Double) newFluidGradient;

            Object newTubingID = well.get("tubingID");
            if (newTubingID != null) setTubingID((Double) newTubingID);

            Object newTubingOD = well.get("tubingOD");
            if (newTubingOD != null) setTubingOD((Double) newTubingOD);

            Object newTubingAnchorDepth = well.get("tubingAnchorDepth");
            if (newTubingAnchorDepth != null) tubingAnchorDepth = (Double) newTubingAnchorDepth;

            Object newStructuralRating = well.get("structuralRating");
            if (newStructuralRating != null) structuralRating = (Double) newStructuralRating;

            Object newStuffingBoxFriction = well.get("stuffingBoxFriction");
            if (newStuffingBoxFriction != null) setSbfriction((Double) newStuffingBoxFriction);

            Object newTubingHeadPressure = well.get("tubingOD");
            if (newTubingHeadPressure != null) setTubingOD((Double) newTubingHeadPressure);

            JSONArray tapers = (JSONArray) well.get("tapers");
            numTapers = tapers.size();
            for (int i = 0; i < numTapers; i++){
                int currentTaperNum = i + 1;
                JSONObject taperObj = (JSONObject) tapers.get(i);

                Object newLength = taperObj.get("length");
                if (newLength != null) setRodLength(currentTaperNum, (Double) newLength);

                Object newDiameter = taperObj.get("diameter");
                if (newDiameter != null) setRodDiameter(currentTaperNum, (Double) newDiameter);

                Object newMaterial = taperObj.get("material");
                if (newMaterial != null) setRodYM(currentTaperNum, (String) newMaterial);

                Object newDampingFactor = taperObj.get("dampingFactor");
                if (newDampingFactor != null) setDampingFactor(currentTaperNum, (Double) newDampingFactor);
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ParseException e) {
            e.printStackTrace();
        }
        updateTapers();

    }

    public void printTapers(){

        System.out.println("===== " + wellName + " =====");
        System.out.println("--- INPUT PARAMETERS ---");

        V2_AsciiTable inputTable = new V2_AsciiTable();
        inputTable.addRule();
        inputTable.addRow("DeltaT", getDt());
        inputTable.addStrongRule();
        inputTable.addRow("Fluid Gradient", getFluidGradient());
        inputTable.addRow("Pump Diameter", getPumpDiameter());
        inputTable.addRow("Stuffing Box Friction", getSbfriction());
        inputTable.addRow("Friction Estimate", getFrictionEstimate());
        inputTable.addRule();
        inputTable.addRow("Tubing Anchor Depth", getTubingAnchorDepth());
        inputTable.addRow("Tubing ID", getTubingID());
        inputTable.addRow("Tubing OD", getTubingOD());
        inputTable.addRule();
        inputTable.addRow("Number of Tapers", getNumTapers());
        inputTable.addRule();
        inputTable.addRow("Buoyant Force Total", buoyantForceTotal);
        inputTable.addRow("Weight Data Total", weightDataTotal);
        inputTable.addRow("Annular Force Data Total", annularForceDataTotal);
        inputTable.addRow("Rod Depth Total", rodDepthTotal);
        inputTable.addRow("Rod Weight Air Total", rodWeightAirTotal);
        inputTable.addRow("Rod Weight Fluid Total", rodWeightFluidTotal);
        inputTable.addRule();
        V2_AsciiTableRenderer rend = new V2_AsciiTableRenderer();
        rend.setTheme(V2_E_TableThemes.UTF_LIGHT.get());
        rend.setWidth(new WidthAbsoluteEven(50));
        RenderedTable rt = rend.render(inputTable);
        System.out.println(rt);
        System.out.println();

        for(int i = 1; i <= numTapers; i++){
            V2_AsciiTable taperTable = new V2_AsciiTable();
            taperTable.addRule();
            taperTable.addRow("Taper", i);
            taperTable.addStrongRule();
            taperTable.addRow("Rod Length", getRodLength(i));
            taperTable.addRow("Rod Diameter", getRodDiameter(i));
            taperTable.addRow("Rod Damping Factor", getDampingFactor(i));
            taperTable.addRow("Rod Material", getRodMaterial(i));
            taperTable.addRow("Rod Young's Modulus", getRodYM(i));
            taperTable.addRule();
            taperTable.addRow("a", a[i]);
            taperTable.addRow("Area", area[i]);
            taperTable.addRow("Pressure", pressure[i]);
            taperTable.addRow("Buoyant Force", buoyantForce[i]);
            taperTable.addRow("Stretch", stretch[i]);
            taperTable.addRow("WeightData", weightData[i]);
            taperTable.addRow("Annular Force Data", annularForceData[i]);
            taperTable.addRow("force", force[i]);
            taperTable.addRow("Alpha", alpha[i]);
            taperTable.addRow("xOverA", xOverA[i]);
            taperTable.addRow("Factor", factor[i]);
            taperTable.addRow("Lag Index", lagIndex[i]);
            taperTable.addRow("Length Required", lengthRequired[i]);
            taperTable.addRow("Center Point", centerPoint[i]);
            taperTable.addRow("Rod Depth", rodDepth[i]);
            taperTable.addRow("Rod Weight in Air", rodWeightAir[i]);
            taperTable.addRow("Rod Weight in Fluid", rodWeightFluid[i]);
            taperTable.addRule();
            rend.setTheme(V2_E_TableThemes.UTF_LIGHT.get());
            rend.setWidth(new WidthAbsoluteEven(50));
            rt = rend.render(taperTable);
            System.out.println(rt);
            System.out.println();
        }

    };

    public void updateTapers(){
        nT1 = numTapers + 1;

        // start by setting everything to 0
        a[0] = 0.0;
        area[0] = 0.0;
        area[numTapers + 1] = 0.0;
        pressure[0] = tubingHeadPressure;
        buoyantForce[0] = 0.0;
        stretch[0] = 0.0;
        weightData[0] = 0.0;
        annularForceData[0] = 0.0;
        force[0] = 0.0;
        alpha[0] = 0.0;
        xOverA[0] = 0.0;
        factor[0] = 0.0;
        lagIndex[0] = 0;
        lengthRequired[0] = 0;
        centerPoint[0] = 0;
        rodDepth[0] = 0.0;
        rodWeightAir[0] = 0.0;
        rodWeightFluid[0] = 0.0;
        buoyantForceTotal = 0.0;
        weightDataTotal = 0.0;
        annularForceDataTotal = 0.0;
        rodDepthTotal = 0.0;
        rodWeightAirTotal = 0.0;
        rodWeightFluidTotal = 0.0;

        for(int i = 1; i < nT1; i++){
            rodWeightPerFoot[i] = lookupRodWeightPerFoot(rodYM[i], rodDiameter[i]);
        }

        for (int area_i = 1; area_i < nT1; area_i++)
        {
            area[area_i] = (Math.PI / 4) * pow(rodDiameter[area_i], 2);
        }

        for (int i = 1; i < nT1; i++)
        {
            a[i] = 1000 * sqrt(32.2 * rodYM[i] * area[i] / rodWeightPerFoot[i]);
            rodDepth[i] = rodDepth[i - 1] + rodLength[i];
            pressure[i] = pressure[i - 1] + fluidGradient * rodLength[i];
            buoyantForce[i] = pressure[i] * (area[i + 1] - area[i]);
            rodWeightAir[i] = rodWeightPerFoot[i] * rodLength[i];
            rodWeightFluid[i] = rodWeightAir[i] + buoyantForce[i];
        }
        for (int j = 1; j < nT1; j++)
        {
            for (int k = j + 1; k < nT1; k++)
            {
                weightData[j] += rodWeightAir[k]; // how much weight is felt on each taper
            }
            for (int l = j; l < numTapers; l++)
            {
                annularForceData[j] += -buoyantForce[l]; //buoyant force exerted on each taper
            }
            force[j] = (-area[numTapers] * pressure[numTapers]) + weightData[j] - annularForceData[j];
            alpha[j] = (force[j] + rodWeightAir[j]) / (rodYM[j] * pow(10, 6) * area[j]);
            stretch[j] = (stretch[j - 1] + (alpha[j] * rodLength[j])) - ((rodWeightPerFoot[j] * pow(rodLength[j], 2)) / (2 * rodYM[j] * pow(10, 6) * area[j]));
        }

        for (int m = 1; m < nT1; m++)
        {
            xOverA[m] = rodLength[m] / a[m];
            lagIndex[m] = (int)(rodLength[m] / (a[m] * dt));
            factor[m] = (xOverA[m] - lagIndex[m] * dt) / dt;
            centerPoint[m] = lagIndex[m] + 2;
            lengthRequired[m] = 2 * (lagIndex[m] + 1) + 1;
        }

        for (int t = 0; t < nT1; t++)
        {
            buoyantForceTotal += buoyantForce[t];
            rodWeightAirTotal += rodWeightAir[t];
            rodWeightFluidTotal += rodWeightFluid[t];
            rodDepthTotal += rodLength[t];
            annularForceDataTotal += annularForceData[t];
            weightDataTotal += weightData[t];
        }

        theoreticalMaxFluidLoad = fluidGradient* rodDepthTotal* pumpArea;
        frictionEstimate = 0.10 * rodDepthTotal;
    }

    private double position(int p)
    //generate the downhole position of the given rod part
    {
        loadBefore = 0;
        loadAfter = 0;
        loadBefore3 = 0;
        loadAfter3 = 0;

        //Temporary Variables
        double a1 = a[p];
        double cd = c[p];
        double fact = factor[p];
        double rodLengthp = rodLength[p];
        int lagInd = lagIndex[p];
        double Y1 = rodYM[p] * pow(10, 6);
        double A1 = area[p];
        int centerOfArray = centerPoint[p];

        int iBefore = centerOfArray - lagInd;
        int iAfter = centerOfArray + lagInd;

        double pumpPOS = 0;
        //Do work
        pumpPOS = exp((cd * rodLengthp) / (2 * a1)) * (topPosArray[p][iAfter] + fact * (topPosArray[p][ iAfter + 1] - topPosArray[p][iAfter]));
        pumpPOS += exp(-(cd * rodLengthp) / (2 * a1)) * (topPosArray[p][iBefore] + fact * (topPosArray[p][iBefore - 1] - topPosArray[p][iBefore]));
        pumpPOS = pumpPOS / 2;
        double insideIntegral = 0;
        int q = 1;
        while (q < 2 * lagInd - 1)
        {
            insideIntegral += dt * (1 / (Y1 * A1)) * (exp(-(cd * (lagInd - q) * dt) / 2) * topLoadArray[p][iBefore + q]);
            q++;
        }
        insideIntegral += 0.5 * dt * (1 / (Y1 * A1)) * (exp(-(cd * lagInd * dt) / 2) * topLoadArray[p][iBefore] + exp(-(cd * (-lagInd) * dt) / 2) * topLoadArray[p][iAfter]);

        loadBefore = exp(-(cd * lagInd * dt) / 2) * topLoadArray[p][iBefore] + fact * (exp(-(cd * (lagInd + 1) * dt) / 2) * topLoadArray[p][iBefore - 1] - exp(-(cd * lagInd * dt) / 2) * topLoadArray[p][iBefore]);
        loadAfter = exp(-(cd * (-lagInd) * dt) / 2) * topLoadArray[p][iAfter] + fact * (exp(-(cd * (-lagInd - 1) * dt) / 2) * topLoadArray[p][iAfter + 1] - exp(-(cd * (-lagInd) * dt) / 2) * topLoadArray[p][iAfter]);
        insideIntegral += 0.5 * fact * dt * (1 / (Y1 * A1)) * (loadBefore + exp(-(cd * (lagInd) * dt) / 2) * topLoadArray[p][iBefore]);
        insideIntegral += 0.5 * fact * dt * (1 / (Y1 * A1)) * (loadAfter + exp(-(cd * (-lagInd) * dt) / 2) * topLoadArray[p][iAfter]);
        insideIntegral = 0.5 * a1 * insideIntegral;
        pumpPOS += insideIntegral;

        insideIntegral = 0;
        int r = 1;
        while (r < 2 * lagInd - 1)
        {
            insideIntegral += dt * (exp(-(cd * lagInd - r) * dt) / 2) * (topPosArray[p][iBefore + r]);
            r++;
        }
        insideIntegral += 0.5 * dt * (exp(-(cd * lagInd * dt) / 2) * topPosArray[p][iBefore] + exp(-(cd * (-lagInd) * dt) / 2) * topPosArray[p][iAfter]);
        loadBefore3 = exp(-(cd * (lagInd) * dt) / 2) * topPosArray[p][iBefore] + fact * (exp(-(cd * (lagInd + 1) * dt) / 2) * topPosArray[p][iBefore] - exp(-(cd * (lagInd) * dt) / 2) * topPosArray[p][iBefore]);
        loadAfter3 = exp(-(cd * (-lagInd) * dt) / 2) * topPosArray[p][iAfter] + fact * (exp(-(cd * (-lagInd - 1) * dt) / 2) * topPosArray[p][iAfter] - exp(-(cd * (-lagInd) * dt) / 2) * topPosArray[p][iAfter]);
        insideIntegral += 0.5 * fact * dt * (loadBefore3 + exp(-(cd * (lagInd) * dt) / 2) * topPosArray[p][iBefore]);
        insideIntegral += 0.5 * fact * dt * (loadAfter3 + exp(-(cd * (-lagInd) * dt) / 2) * topPosArray[p][iAfter]);
        insideIntegral = -((cd * rodLengthp) / 4) * (0.5 * (cd / (2 * a1))) * insideIntegral;
        pumpPOS += insideIntegral;
        //printf("Position: %f\n", pumpPOS);
        return pumpPOS;
    };


    private double load(int s)
    //calculate the downhole load of the given rod part
    {
        //temporary variables
        double a1 = a[s];
        double cd = c[s];
        double rodLengths = rodLength[s];
        int lagInd = lagIndex[s];
        double Y1 = rodYM[s] * pow(10, 6);
        double A1 = area[s];
        int centerOfArray = centerPoint[s];

        int iBefore = centerOfArray - lagInd;
        int iAfter = centerOfArray + lagInd;

        double pumpLOAD = 0;
        pumpLOAD = 0.5 * (a1 / (Y1 * A1)) * (1 / a1) * (loadBefore + loadAfter);
        pumpLOAD += -((cd * rodLengths) / 4) * (0.5 * (cd / (2 * a1))) * (1 / a1) * (loadBefore3 + loadAfter3);

        double firstPart = 0;
        double pointAfter = (topPosArray[s][iAfter + 1] - topPosArray[s][iAfter - 1]) / (2 * dt);
        double pointBefore = (topPosArray[s][iBefore + 1] - topPosArray[s][iBefore - 1]) / (2 * dt);
        firstPart = (exp((cd * rodLengths) / (2 * a1)) * pointAfter - exp(-(cd * rodLengths) / (2 * a1)) * pointBefore) / (2 * a1);
        firstPart += (cd * exp((cd * rodLengths) / (2 * a1)) * topPosArray[s][iAfter] - cd * exp((-cd * rodLengths) / (2 * a1)) * topPosArray[s][iBefore]) / (4 * a1);
        pumpLOAD = Y1 * A1 * (firstPart + pumpLOAD);
        //printf("Load: %f\n", pumpLOAD);
        return pumpLOAD;
    };

    private LPStatus calc(double sPosition, double sLoad){
        boolean useShift = false;
        int loadMult = 1;
        int tapersAllowed = 1;
        double dPosition = 0;
        double dLoad = 0;
        int status = BAD_STATUS;
        for (int ii = 1; ii < lengthRequired[1] + 1; ii++)
        {
            topPosArray[1][ii - 1] = topPosArray[1][ii];
            topLoadArray[1][ii - 1] = topLoadArray[1][ii];
        }
        topPosArray[1][lengthRequired[1]] = -(sPosition / 12); //stores current position in feet

        if (sPosition > sPositionPrevious)
        {
            topLoadArray[1][lengthRequired[1]] = loadMult * (sLoad - rodWeightFluidTotal) - sbfriction;
        }
        else
        {
            topLoadArray[1][lengthRequired[1]] = loadMult * (sLoad - rodWeightFluidTotal) + sbfriction;
        }
        sPositionPrevious = sPosition;
        int j = 1;
        while (j <= tapersAllowed)
        {
            count[j]++;
            if (count[j] >= lengthRequired[j])
            {
                if ((j + 1) <= numTapers)
                {
                    for (int jj = 2; jj < lengthRequired[j + 1] + 1; jj++)
                    {
                        topPosArray[j + 1][jj - 1] = topPosArray[j + 1][jj];
                        topLoadArray[j + 1][jj - 1] = topLoadArray[j + 1][jj];
                    }
                    topPosArray[j + 1][lengthRequired[j + 1]] = position(j);
                    topLoadArray[j + 1][lengthRequired[j + 1]] = load(j);
                }
                else
                {
                    if (useShift)
                    {
                        dPosition = -12 * (position(j) + stretch[numTapers]);
                    }
                    else
                    {
                        dPosition = -12 * position(j);
                    }
                    dLoad = load(j) + force[numTapers];
                    status = GOOD_STATUS;
                }
                count[j]--;
                tapersAllowed += 1;
                if (tapersAllowed > numTapers)
                {
                    tapersAllowed = numTapers;
                }
            }
            j++;
        }
        LPStatus downholeValues = new LPStatus(dPosition, dLoad, status);
        return downholeValues;
    };

    public void endOfStroke(){
        currentCard.setNumPointsUsed(pointCounter + 1);
        currentCard.calcStrokeData(150, fluidGradient,
                rodDepthTotal, tubingAnchorDepth,
                tubingCrossSectionalArea, pumpArea,
                frictionEstimate, structuralRating, fluidWaterRatio, fluidOilRatio, fluidGasRatio);
        for (int j = 98; j >= 0; j--) {
            cardStorage[j + 1] = cardStorage[j];
        }
        cardStorage[0] = currentCard;
        currentCard.printCard("csv", true);
        strokesSinceStart++;
        strokesToday++;
        strokesLifetime++;

        strokeSpeed.update(currentCard.getStrokeSpeed());
        downholeGrossStroke.update(currentCard.getDownholeGrossStrokeLength());
        downholeNetStroke.update(currentCard.getDownholeNetStrokeLength());
        fluidLevel.update(currentCard.getFluidLevel());
        fluidLoad.update(currentCard.getFluidLoad());
        peakPolishedRodLoad.update(currentCard.getSurfaceLoadMax().getLoad());
        minPolishedRodLoad.update(currentCard.getSurfaceLoadMin().getLoad());
        polishedRodHP.update(currentCard.getPolishedRodHorsepower());
        pumpHP.update(currentCard.getPumpHorsepower());
        fluidProduced.update(currentCard.getFluidBBLMoved());
        oilProduced.update(currentCard.getOilBBLMoved());
        waterProduced.update(currentCard.getWaterBBLMoved());
        gasProduced.update(currentCard.getGasMCFMoved());
        pumpIntakePressure.update(currentCard.getPumpIntakePressure());
        surfaceStrokeLength.update(currentCard.getSurfaceStrokeLength());
        tubingMovement.update(currentCard.getTubingMovement());

        currentCard = new Card(strokesLifetime);
        pointCounter = -1;
        if (strokesSinceStart > startupStrokes){
            runStatus = RUNSTATUS_RUNNING;
        }
        if(isNewDay()){
            strokeSpeed.endOfDay();
            downholeGrossStroke.endOfDay();
            downholeNetStroke.endOfDay();
            fluidLevel.endOfDay();
            fluidLoad.endOfDay();
            inflowRate.endOfDay();
            peakPolishedRodLoad.endOfDay();
            minPolishedRodLoad.endOfDay();
            percentRun.endOfDay();
            polishedRodHP.endOfDay();
            pumpHP.endOfDay();
            fluidProduced.endOfDay();
            oilProduced.endOfDay();
            waterProduced.endOfDay();
            gasProduced.endOfDay();
            pumpIntakePressure.endOfDay();
            surfaceStrokeLength.endOfDay();
            tubingMovement.endOfDay();
        }

    }

    public void eval(){
        checkSafeties();
        currentSurfacePosition = inclinometer.readScaled();
        currentSurfaceLoad = loadCell.readScaled();
        LPStatus lastPoint = calc(currentSurfacePosition, currentSurfaceLoad);
        if(runStatus == RUNSTATUS_STARTING || runStatus == RUNSTATUS_RUNNING) {
            if (lastPoint.getStatus() == GOOD_STATUS) {
                currentCard.setSurfacePosition(pointCounter, currentSurfacePosition);
                currentCard.setSurfaceLoad(pointCounter, currentSurfaceLoad);
                currentCard.setDownholePosition(pointCounter, lastPoint.getPosition());
                currentCard.setDownholeLoad(pointCounter, lastPoint.getLoad());
            }

            if (inclinometer.getHistory(0) > inclinometer.getHistory(1))
                direction = DIRECTION_UP;
            else if (inclinometer.getHistory(0) < inclinometer.getHistory(1))
                direction = DIRECTION_DOWN;

            if (direction == DIRECTION_UP && lastDirection == DIRECTION_DOWN && pointCounter > 0) {
                currentCard.setNumPointsUsed(pointCounter + 1);
                currentCard.calcStrokeData(150, fluidGradient,
                        rodDepthTotal, tubingAnchorDepth,
                        tubingCrossSectionalArea, pumpArea,
                        frictionEstimate, structuralRating, fluidWaterRatio, fluidOilRatio, fluidGasRatio);
                for (int j = 98; j >= 0; j--) {
                    cardStorage[j + 1] = cardStorage[j];
                }
                cardStorage[0] = currentCard;
                currentCard.printCard("none", true);
                System.out.println("Cards in DB: " + db.newCard(currentCard));
                strokesSinceStart++;
                strokesToday++;
                strokesLifetime++;

                strokeSpeed.update(currentCard.getStrokeSpeed());
                downholeGrossStroke.update(currentCard.getDownholeGrossStrokeLength());
                downholeNetStroke.update(currentCard.getDownholeNetStrokeLength());
                fluidLevel.update(currentCard.getFluidLevel());
                fluidLoad.update(currentCard.getFluidLoad());
                peakPolishedRodLoad.update(currentCard.getSurfaceLoadMax().getLoad());
                minPolishedRodLoad.update(currentCard.getSurfaceLoadMin().getLoad());
                polishedRodHP.update(currentCard.getPolishedRodHorsepower());
                pumpHP.update(currentCard.getPumpHorsepower());
                fluidProduced.update(currentCard.getFluidBBLMoved());
                oilProduced.update(currentCard.getOilBBLMoved());
                waterProduced.update(currentCard.getWaterBBLMoved());
                gasProduced.update(currentCard.getGasMCFMoved());
                pumpIntakePressure.update(currentCard.getPumpIntakePressure());
                surfaceStrokeLength.update(currentCard.getSurfaceStrokeLength());
                tubingMovement.update(currentCard.getTubingMovement());

                currentCard = new Card(strokesLifetime);
                pointCounter = -1;
                if (strokesSinceStart > startupStrokes) {
                    runStatus = RUNSTATUS_RUNNING;
                }
            }
            lastDirection = direction;
            pointCounter++;
        }
        if(isNewDay()){
            strokeSpeed.endOfDay();
            downholeGrossStroke.endOfDay();
            downholeNetStroke.endOfDay();
            fluidLevel.endOfDay();
            fluidLoad.endOfDay();
            inflowRate.endOfDay();
            peakPolishedRodLoad.endOfDay();
            minPolishedRodLoad.endOfDay();
            percentRun.endOfDay();
            polishedRodHP.endOfDay();
            pumpHP.endOfDay();
            fluidProduced.endOfDay();
            oilProduced.endOfDay();
            waterProduced.endOfDay();
            gasProduced.endOfDay();
            pumpIntakePressure.endOfDay();
            surfaceStrokeLength.endOfDay();
            tubingMovement.endOfDay();
        }
    }



    public void eval(int simPoint){
        checkSafeties();
        currentSurfacePosition = inclinometer.readScaledSim(sim.getPositionAtIndex(simPoint));
        currentSurfaceLoad = loadCell.readScaledSim(sim.getLoadAtIndex(simPoint));

        LPStatus lastPoint = calc(currentSurfacePosition, currentSurfaceLoad);
        if (lastPoint.getStatus() == GOOD_STATUS){
            currentDownholePosition = lastPoint.getPosition();
            currentDownholeLoad = lastPoint.getLoad();
        }
        if(runStatus == RUNSTATUS_STARTING || runStatus == RUNSTATUS_RUNNING) {
            if (lastPoint.getStatus() == GOOD_STATUS) {
                currentCard.setSurfacePosition(pointCounter, currentSurfacePosition);
                currentCard.setSurfaceLoad(pointCounter, currentSurfaceLoad);
                currentCard.setDownholePosition(pointCounter, currentDownholePosition);
                currentCard.setDownholeLoad(pointCounter, currentDownholeLoad);
            }

            if (inclinometer.getHistory(0) > inclinometer.getHistory(1))
                direction = DIRECTION_UP;
            else if (inclinometer.getHistory(0) < inclinometer.getHistory(1))
                direction = DIRECTION_DOWN;

            if (direction == DIRECTION_UP && lastDirection == DIRECTION_DOWN && pointCounter > 0) {
                endOfStroke();
            }
            lastDirection = direction;
            pointCounter++;
        }

        if (runStatus == RUNSTATUS_RUNNING || runStatus == RUNSTATUS_STARTING){
            runCommand.write(1);
        } else {
            runCommand.write(0);
        }
    }
    public void printTotals(){

        V2_AsciiTable taperTable = new V2_AsciiTable();
        taperTable.addRule();
        taperTable.addRow("Measuremnt", "Average", "Total");
        taperTable.addStrongRule();
        taperTable.addRow("Stroke Speed", strokeSpeed.getAverage(), strokeSpeed.getTotal());
        taperTable.addRow("DH Gross Stroke", downholeGrossStroke.getAverage(), downholeGrossStroke.getTotal());
        taperTable.addRow("DH Net Stroke", downholeNetStroke.getAverage(), downholeNetStroke.getTotal());
        taperTable.addRow("Fluid Level", fluidLevel.getAverage(), fluidLevel.getTotal());
        taperTable.addRow("Fluid Load", fluidLoad.getAverage(), fluidLoad.getTotal());
        taperTable.addRow("Peak PR Load", peakPolishedRodLoad.getAverage(), peakPolishedRodLoad.getTotal());
        taperTable.addRow("Min PR Load", minPolishedRodLoad.getAverage(), minPolishedRodLoad.getTotal());
        taperTable.addRow("PR HP", polishedRodHP.getAverage(), polishedRodHP.getTotal());
        taperTable.addRow("Pump HP", pumpHP.getAverage(), pumpHP.getTotal());
        taperTable.addRow("Fluid Produced", fluidProduced.getAverage(), fluidProduced.getTotal());
        taperTable.addRow("Oil Produced", oilProduced.getAverage(), oilProduced.getTotal());
        taperTable.addRow("Water Produced", waterProduced.getAverage(), waterProduced.getTotal());
        taperTable.addRow("Gas Produced", gasProduced.getAverage(), gasProduced.getTotal());
        taperTable.addRow("Pump Intake Pressure", pumpIntakePressure.getAverage(), pumpIntakePressure.getTotal());
        taperTable.addRow("Surf Stroke Length", surfaceStrokeLength.getAverage(), surfaceStrokeLength.getTotal());
        taperTable.addRow("Tubing Movement", tubingMovement.getAverage(), tubingMovement.getTotal());
        taperTable.addRule();
        V2_AsciiTableRenderer rend = new V2_AsciiTableRenderer();
        rend.setTheme(V2_E_TableThemes.UTF_LIGHT.get());
        rend.setWidth(new WidthAbsoluteEven(100));
        RenderedTable rt = rend.render(taperTable);
        System.out.println(rt);
        System.out.println();
    }

    public void allOutputsOff(){
        runCommand.write(0, true);
    }

    public static void main( String[] args ){
        Well thisWell = new Well("Barney", args[1], 99, 99, 99);
        thisWell.parseJSONFile(args[0]);
        thisWell.printTapers();
    }


}


