using System;
using System.Collections.Generic;
using FluidSim.Interfaces;

namespace FluidSim.Components
{
    public class Cylinder : IComponent
    {
        public Port IntakePort { get; }
        public Port ExhaustPort { get; }
        public Crankshaft Crankshaft { get; }

        private readonly Port[] _ports;
        IReadOnlyList<Port> IComponent.Ports => _ports;

        // Geometry
        public double Bore { get; }
        public double Stroke { get; }
        public double ConRodLength { get; }
        public double CompressionRatio { get; }

        // Valve timings (degrees, 0 = TDC compression, 720° full cycle)
        public double IVO { get; }
        public double IVC { get; }
        public double EVO { get; }
        public double EVC { get; }

        // Valve geometry
        public double IntakeValveDiameter { get; set; } = 0.030;
        public double ExhaustValveDiameter { get; set; } = 0.028;
        public double IntakeValveLift { get; set; } = 0.005;
        public double ExhaustValveLift { get; set; } = 0.005;

        public double IntakeValveMaxArea => Math.PI * IntakeValveDiameter * IntakeValveLift;
        public double ExhaustValveMaxArea => Math.PI * ExhaustValveDiameter * ExhaustValveLift;

        // Ignition and combustion
        public double SparkAdvance { get; set; } = 20.0;
        public double WiebeA { get; set; } = 5.0;
        public double WiebeM { get; set; } = 2.0;
        public double WiebeDuration { get; set; } = 60.0;
        public double WiebeStart { get; set; } = 5.0;

        // Fuel
        public double StoichiometricAFR { get; set; } = 14.7;
        public double FuelLowerHeatingValue { get; set; } = 44e6;

        // Cycle‑to‑cycle randomness
        public double EnergyVariationFraction { get; set; } = 0.05;
        public double MisfireProbability { get; set; } = 0.01;

        // Heat loss
        public double CylinderWallArea { get; set; } = 0.02;
        public double HeatTransferCoefficient { get; set; } = 100.0;
        public double AmbientTemperature { get; set; } = 300.0;

        // ---- Multi‑cylinder support ----
        /// <summary>
        /// Phase offset (radians) added to the crankshaft angle for this cylinder.
        /// Used for multi‑cylinder engines; set to 0 for single‑cylinder.
        /// </summary>
        public double PhaseOffset { get; set; } = 0.0;

        // State (public for drawing)
        public double Volume => cylinderVolume;
        public double Pressure => (Gamma - 1.0) * cylinderEnergy / Math.Max(cylinderVolume, 1e-12);
        public double Temperature => Pressure / Math.Max(Density * GasConstant, 1e-12);
        public double Density => Mass / Math.Max(cylinderVolume, 1e-12);
        public double Mass => _airMass + _exhaustMass;
        public double AirFraction => _airMass / Math.Max(Mass, 1e-12);
        public double PistonFraction => (cylinderVolume - clearanceVolume) / SweptVolume;

        private double cylinderVolume;
        private double cylinderEnergy;
        private double _airMass;
        private double _exhaustMass;
        private double trappedAirMass;
        private double fuelMass;
        private double burnFraction;
        private bool combustionActive;
        private bool fuelInjected;

        private double _energyFactor = 1.0;
        private readonly Random _random = new Random();

        private const double Gamma = 1.4;
        private const double GasConstant = 287.0;

        private const double MaxPressurePa = 200e5;
        private const double MaxTemperatureK = 3500.0;

        public Cylinder(double bore, double stroke, double conRodLength, double compressionRatio,
                        double ivo, double ivc, double evo, double evc, Crankshaft crankshaft)
        {
            Bore = bore;
            Stroke = stroke;
            ConRodLength = conRodLength;
            CompressionRatio = compressionRatio;
            IVO = ivo;
            IVC = ivc;
            EVO = evo;
            EVC = evc;

            Crankshaft = crankshaft ?? throw new ArgumentNullException(nameof(crankshaft));

            cylinderVolume = clearanceVolume;
            double initRho = 1.225;
            _airMass = initRho * clearanceVolume;
            _exhaustMass = 0.0;
            cylinderEnergy = 101325.0 * clearanceVolume / (Gamma - 1.0);

            IntakePort = new Port { Owner = this };
            ExhaustPort = new Port { Owner = this };
            _ports = new[] { IntakePort, ExhaustPort };
        }

        // Derived volumes
        private double SweptVolume => Math.PI * 0.25 * Bore * Bore * Stroke;
        private double clearanceVolume => SweptVolume / (CompressionRatio - 1.0);
        private double CrankRadius => Stroke / 2.0;
        private double Obliquity => CrankRadius / ConRodLength;

        // Offset-aware crank angle in degrees
        private double CrankDeg =>
            ((Crankshaft.CrankAngle + PhaseOffset) % (4.0 * Math.PI)) * 180.0 / Math.PI % 720.0;

        public double ComputeVolume(double thetaRad)
        {
            double r = CrankRadius;
            double l = ConRodLength;
            double cosTh = Math.Cos(thetaRad);
            double sinTh = Math.Sin(thetaRad);
            double term = Math.Sqrt(1.0 - Obliquity * Obliquity * sinTh * sinTh);
            double x = r * (1.0 - cosTh) + l * (1.0 - term);
            double area = Math.PI * 0.25 * Bore * Bore;
            return clearanceVolume + area * x;
        }

        private double ValveLift(double thetaDeg, double opens, double closes, double peakLift)
        {
            double deg = thetaDeg % 720.0;
            if (deg < 0) deg += 720.0;

            double duration = closes - opens;
            if (duration <= 0) return 0.0;

            double rampDur = duration * 0.25;
            double holdDur = duration - 2.0 * rampDur;

            if (deg >= opens && deg < opens + rampDur)
            {
                double t = (deg - opens) / rampDur;
                return peakLift * t * t * (3.0 - 2.0 * t);
            }
            else if (deg >= opens + rampDur && deg < opens + rampDur + holdDur)
            {
                return peakLift;
            }
            else if (deg >= opens + rampDur + holdDur && deg <= closes)
            {
                double t = (deg - (opens + rampDur + holdDur)) / rampDur;
                return peakLift * (1.0 - t) * (1.0 - t) * (1.0 + 2.0 * t);
            }
            return 0.0;
        }

        public double IntakeValveArea =>
            Math.PI * IntakeValveDiameter * ValveLift(CrankDeg, IVO, IVC, IntakeValveLift);

        public double ExhaustValveArea =>
            Math.PI * ExhaustValveDiameter * ValveLift(CrankDeg, EVO, EVC, ExhaustValveLift);

        private double Wiebe(double angleSinceSpark)
        {
            if (angleSinceSpark < WiebeStart) return 0.0;
            double phi = (angleSinceSpark - WiebeStart) / WiebeDuration;
            if (phi <= 0) return 0.0;
            return 1.0 - Math.Exp(-WiebeA * Math.Pow(phi, WiebeM + 1));
        }

        public void PreStep(double dt)
        {
            double prevVolume = cylinderVolume;

            // ----- Use phase‑offset crank angle for this cylinder -----
            double crankAngleRad = Crankshaft.CrankAngle + PhaseOffset;
            cylinderVolume = ComputeVolume(crankAngleRad);

            double dV = cylinderVolume - prevVolume;

            // Piston torque
            double pRel = Pressure - 101325.0;
            double sinTh = Math.Sin(crankAngleRad);
            double cosTh = Math.Cos(crankAngleRad);
            double term = Math.Sqrt(1.0 - Obliquity * Obliquity * sinTh * sinTh);
            double dxdtheta = CrankRadius * sinTh * (1.0 + Obliquity * cosTh / term);
            double pistonArea = Math.PI * 0.25 * Bore * Bore;
            double torque = pRel * pistonArea * dxdtheta;
            Crankshaft.AddTorque(torque);

            cylinderEnergy -= Pressure * dV;

            // Also use offset angle for event detection
            double crankshaftPrevAngle = Crankshaft.PreviousAngle;
            double prevDeg = (crankshaftPrevAngle + PhaseOffset) * 180.0 / Math.PI % 720.0;
            double currDeg = crankAngleRad * 180.0 / Math.PI % 720.0;

            // ----- Intake closing: capture trapped air mass and compute fuel -----
            if (prevDeg >= IVO && prevDeg < IVC && currDeg >= IVC)
            {
                trappedAirMass = _airMass;
                fuelMass = trappedAirMass / StoichiometricAFR;
                fuelInjected = true;
            }

            // ----- Spark ignition -----
            double sparkAngle = 0.0 - SparkAdvance;
            if (sparkAngle < 0) sparkAngle += 720.0;

            bool crossedSpark = (prevDeg < sparkAngle && currDeg >= sparkAngle) ||
                                (prevDeg > sparkAngle + 360.0 && currDeg < sparkAngle);
            if (crossedSpark && !combustionActive && fuelInjected)
            {
                bool misfire = _random.NextDouble() < MisfireProbability;
                if (misfire)
                {
                    combustionActive = false;
                }
                else
                {
                    combustionActive = true;
                    burnFraction = 0.0;
                    double range = EnergyVariationFraction;
                    _energyFactor = 1.0 + range * (2.0 * _random.NextDouble() - 1.0);
                }
            }

            // ----- Combustion progress -----
            if (combustionActive)
            {
                double angleSinceSpark = currDeg - sparkAngle;
                if (angleSinceSpark < 0) angleSinceSpark += 720.0;
                double newFraction = Wiebe(angleSinceSpark);

                if (newFraction >= 1.0 || angleSinceSpark > (WiebeDuration + WiebeStart + SparkAdvance))
                {
                    newFraction = 1.0;
                    combustionActive = false;
                    double totalMass = _airMass + _exhaustMass;
                    _airMass = 0.0;
                    _exhaustMass = totalMass;
                }

                double dFraction = newFraction - burnFraction;
                if (dFraction > 0)
                {
                    double dQ = fuelMass * FuelLowerHeatingValue * _energyFactor * dFraction;
                    cylinderEnergy += dQ;
                    _exhaustMass += fuelMass * dFraction;
                    burnFraction = newFraction;
                }
            }

            // ----- Heat loss -----
            double dQ_loss = HeatTransferCoefficient * CylinderWallArea *
                             (Temperature - AmbientTemperature) * dt;
            cylinderEnergy -= dQ_loss;

            // Update port states
            double p = Pressure, rho = Density, T = Temperature;
            double h = Gamma / (Gamma - 1.0) * p / Math.Max(rho, 1e-12);
            double af = AirFraction;

            IntakePort.Pressure = p;
            IntakePort.Density = rho;
            IntakePort.Temperature = T;
            IntakePort.SpecificEnthalpy = h;
            IntakePort.AirFraction = af;

            ExhaustPort.Pressure = p;
            ExhaustPort.Density = rho;
            ExhaustPort.Temperature = T;
            ExhaustPort.SpecificEnthalpy = h;
            ExhaustPort.AirFraction = af;
        }

        public void UpdateState(double dt)
        {
            double dmAir = 0.0, dmExhaust = 0.0, dE = 0.0;

            foreach (var port in _ports)
            {
                double mdot = port.MassFlowRate;
                double af = mdot >= 0 ? port.AirFraction : AirFraction;
                dmAir += mdot * af * dt;
                dmExhaust += mdot * (1.0 - af) * dt;
                dE += mdot * port.SpecificEnthalpy * dt;
            }

            _airMass += dmAir;
            _exhaustMass += dmExhaust;
            cylinderEnergy += dE;

            double V = Math.Max(cylinderVolume, 1e-12);

            double currentP = (Gamma - 1.0) * cylinderEnergy / V;
            if (currentP > MaxPressurePa)
                cylinderEnergy = MaxPressurePa * V / (Gamma - 1.0);

            double currentRho = (_airMass + _exhaustMass) / V;
            double currentT = currentP / Math.Max(currentRho * GasConstant, 1e-12);
            if (currentT > MaxTemperatureK)
            {
                double pAtTlimit = currentRho * GasConstant * MaxTemperatureK;
                cylinderEnergy = pAtTlimit * V / (Gamma - 1.0);
            }

            double totalMass = _airMass + _exhaustMass;
            if (totalMass < 1e-9)
            {
                _airMass = 1e-9;
                _exhaustMass = 0.0;
                cylinderEnergy = 101325.0 * V / (Gamma - 1.0);
            }
            else if (cylinderEnergy < 0.0)
            {
                cylinderEnergy = 101325.0 * V / (Gamma - 1.0);
            }

            if (_airMass < 0.0) _airMass = 0.0;
            if (_exhaustMass < 0.0) _exhaustMass = 0.0;
        }
    }
}