/*
 * Copyright (c) Contributors, http://opensimulator.org/
 * See CONTRIBUTORS.TXT for a full list of copyright holders.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the OpenSim Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using OpenMetaverse;
using Nini.Config;
using OpenSim.Framework;
using OpenSim.Region.Environment.Interfaces;
using OpenSim.Region.Environment.Scenes;

namespace OpenSim.Region.Environment.Modules
{
    public class SunModule : IRegionModule
    {

        private static readonly log4net.ILog m_log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        private const double SeasonalTilt   =  0.03 * Math.PI;  // A daily shift of approximately 1.7188 degrees
        private const double AverageTilt    = -0.25 * Math.PI;  // A 45 degree tilt
        private const double SunCycle       =  2.0D * Math.PI;  // A perfect circle measured in radians
        private const double SeasonalCycle  =  2.0D * Math.PI;  // Ditto

        //
        //    Per Region Values
        //

        private bool   ready = false;

        // Configurable values
        private string m_mode           = "SL";
        private int    m_frame_mod      = 0;
        private double m_day_length     = 0;
        private int    m_year_length    = 0;
        private double m_day_night      = 0;
        // private double m_longitude      = 0;
        // private double m_latitude       = 0;
        // Configurable defaults                     Defaults close to SL
        private string d_mode           = "SL";
        private int    d_frame_mod      = 100;    // Every 10 seconds (actually less)
        private double d_day_length     = 4;      // A VW day is 4 RW hours long
        private int    d_year_length    = 60;     // There are 60 VW days in a VW year
        private double d_day_night      = 0.45;   // axis offset: ratio of light-to-dark, approx 1:3
        // private double d_longitude      = -73.53;
        // private double d_latitude       = 41.29;

        // Frame counter
        private uint   m_frame          = 0;

        // Cached Scene reference
        private Scene  m_scene          = null;

        // Calculated Once in the lifetime of a region
        private long  TicksToEpoch;              // Elapsed time for 1/1/1970
        private uint   SecondsPerSunCycle;        // Length of a virtual day in RW seconds
        private uint   SecondsPerYear;            // Length of a virtual year in RW seconds
        private double SunSpeed;                  // Rate of passage in radians/second
        private double SeasonSpeed;               // Rate of change for seasonal effects
        // private double HoursToRadians;            // Rate of change for seasonal effects
        private long TicksOffset = 0;                // seconds offset from UTC
        // Calculated every update
        private float  OrbitalPosition;           // Orbital placement at a point in time
        private double HorizonShift;              // Axis offset to skew day and night
        private double TotalDistanceTravelled;    // Distance since beginning of time (in radians)
        private double SeasonalOffset;            // Seaonal variation of tilt
        private float  Magnitude;                 // Normal tilt
        // private double VWTimeRatio;               // VW time as a ratio of real time

        // Working values
        private Vector3 Position = Vector3.Zero;
        private Vector3 Velocity = Vector3.Zero;
        private Quaternion  Tilt = Quaternion.Identity;

        private long LindenHourOffset = 0;
        private bool sunFixed = false;

        private Dictionary<UUID, ulong> m_rootAgents = new Dictionary<UUID, ulong>();

        // Current time in elapsed seconds since Jan 1st 1970
        private ulong CurrentTime
        {
            get {
                return (ulong)(((System.DateTime.Now.Ticks) - TicksToEpoch + TicksOffset + LindenHourOffset)/10000000);
            }
        }

        private float GetLindenEstateHourFromCurrentTime()
        {
            float ticksleftover = ((float)CurrentTime) % ((float)SecondsPerSunCycle);

            float hour = (24 * (ticksleftover / SecondsPerSunCycle)) + 6;

            return hour;
        }

        private void SetTimeByLindenHour(float LindenHour)
        {
            // Linden hour is 24 hours with a 6 hour offset.  6-30

            if (LindenHour - 6 == 0)
            {
                LindenHourOffset = 0;
                return;
            }

            // Remove LindenHourOffset to calculate it from LocalTime
            float ticksleftover = ((float)(((long)(CurrentTime * 10000000) - (long)LindenHourOffset)/ 10000000) % ((float)SecondsPerSunCycle));
            float hour = (24 * (ticksleftover / SecondsPerSunCycle));

            float offsethours = 0;

            if (LindenHour - 6 > hour)
            {
                offsethours = hour + ((LindenHour-6) - hour);
            }
            else
            {
                offsethours = hour - (hour - (LindenHour - 6));
            }
            //m_log.Debug("[OFFSET]: " + hour + " - " + LindenHour + " - " + offsethours.ToString());

            LindenHourOffset = (long)((float)offsethours * (36000000000/m_day_length));
            m_log.Info("[SUN]: Directive from the Estate Tools to set the sun phase to LindenHour " + GetLindenEstateHourFromCurrentTime().ToString());
        }

        // Called immediately after the module is loaded for a given region
        // i.e. Immediately after instance creation.
        public void Initialise(Scene scene, IConfigSource config)
        {
            m_log.Debug("[SUN] Initializing");

            m_scene = scene;

            m_frame = 0;

            TimeZone local = TimeZone.CurrentTimeZone;
            TicksOffset = local.GetUtcOffset(local.ToLocalTime(DateTime.Now)).Ticks;
            m_log.Debug("[SUN] localtime offset is " + TicksOffset);

            // Align ticks with Second Life

            TicksToEpoch = new System.DateTime(1970,1,1).Ticks;

            // Just in case they don't have the stanzas
            try
            {
                // Mode: determines how the sun is handled
                m_mode = config.Configs["Sun"].GetString("mode", d_mode);
                // Mode: determines how the sun is handled
                // m_latitude = config.Configs["Sun"].GetDouble("latitude", d_latitude);
                // Mode: determines how the sun is handled
                // m_longitude = config.Configs["Sun"].GetDouble("longitude", d_longitude);
                // Year length in days
                m_year_length = config.Configs["Sun"].GetInt("year_length", d_year_length);
                // Day length in decimal hours
                m_day_length  = config.Configs["Sun"].GetDouble("day_length", d_day_length);
                // Day to Night Ratio
                m_day_night   = config.Configs["Sun"].GetDouble("day_night_offset", d_day_night);
                // Update frequency in frames
                m_frame_mod   = config.Configs["Sun"].GetInt("update_interval", d_frame_mod);
            }
            catch (Exception e)
            {
                m_log.Debug("[SUN] Configuration access failed, using defaults. Reason: "+e.Message);
                m_mode        = d_mode;
                m_year_length = d_year_length;
                m_day_length  = d_day_length;
                m_day_night   = d_day_night;
                m_frame_mod   = d_frame_mod;
                // m_latitude    = d_latitude;
                // m_longitude   = d_longitude;
            }

            switch (m_mode)
            {
                case "T1":
                default:
                case "SL":
                    // Time taken to complete a cycle (day and season)

                    SecondsPerSunCycle = (uint) (m_day_length * 60 * 60);
                    SecondsPerYear     = (uint) (SecondsPerSunCycle*m_year_length);

                    // Ration of real-to-virtual time

                    // VWTimeRatio        = 24/m_day_length;

                    // Speed of rotation needed to complete a cycle in the
                    // designated period (day and season)

                    SunSpeed           = SunCycle/SecondsPerSunCycle;
                    SeasonSpeed        = SeasonalCycle/SecondsPerYear;

                    // Horizon translation

                    HorizonShift      = m_day_night; // Z axis translation
                    // HoursToRadians    = (SunCycle/24)*VWTimeRatio;

                    //  Insert our event handling hooks

                    scene.EventManager.OnFrame     += SunUpdate;
                    scene.EventManager.OnMakeChildAgent += MakeChildAgent;
                    scene.EventManager.OnAvatarEnteringNewParcel += AvatarEnteringParcel;
                    scene.EventManager.OnClientClosed += ClientLoggedOut;
                    scene.EventManager.OnEstateToolsTimeUpdate += EstateToolsTimeUpdate;
                    scene.EventManager.OnGetSunLindenHour += GetLindenEstateHourFromCurrentTime;

                    ready = true;

                    m_log.Debug("[SUN] Mode is "+m_mode);
                    m_log.Debug("[SUN] Initialization completed. Day is "+SecondsPerSunCycle+" seconds, and year is "+m_year_length+" days");
                    m_log.Debug("[SUN] Axis offset is "+m_day_night);
                    m_log.Debug("[SUN] Positional data updated every "+m_frame_mod+" frames");

                    break;
            }
        }

        public void PostInitialise()
        {
        }

        public void Close()
        {
            ready = false;
            
            // Remove our hooks
            m_scene.EventManager.OnFrame     -= SunUpdate;
            m_scene.EventManager.OnMakeChildAgent -= MakeChildAgent;
            m_scene.EventManager.OnAvatarEnteringNewParcel -= AvatarEnteringParcel;
            m_scene.EventManager.OnClientClosed -= ClientLoggedOut;
            m_scene.EventManager.OnEstateToolsTimeUpdate -= EstateToolsTimeUpdate;
            m_scene.EventManager.OnGetSunLindenHour -= GetLindenEstateHourFromCurrentTime;
        }

        public string Name
        {
            get { return "SunModule"; }
        }

        public bool IsSharedModule
        {
            get { return false; }
        }

        public void SunToClient(IClientAPI client)
        {
            if (m_mode != "T1")
            {
                if (ready)
                {
                    if (!sunFixed)
                        GenSunPos();    // Generate shared values once
                    client.SendSunPos(Position, Velocity, CurrentTime, SecondsPerSunCycle, SecondsPerYear, OrbitalPosition);
                    m_log.Debug("[SUN] Initial update for new client");
                }
            }
        }

        public void SunUpdate()
        {
            if (((m_frame++%m_frame_mod) != 0) || !ready || sunFixed)
            {
                return;
            }

            GenSunPos();        // Generate shared values once

            List<ScenePresence> avatars = m_scene.GetAvatars();
            foreach (ScenePresence avatar in avatars)
            {
                if (!avatar.IsChildAgent)
                    avatar.ControllingClient.SendSunPos(Position, Velocity, CurrentTime, SecondsPerSunCycle, SecondsPerYear, OrbitalPosition);
            }

            // set estate settings for region access to sun position
            m_scene.RegionInfo.RegionSettings.SunVector = Position;
            //m_scene.RegionInfo.EstateSettings.sunHour = GetLindenEstateHourFromCurrentTime();
        }

        public void ForceSunUpdateToAllClients()
        {
            GenSunPos();        // Generate shared values once

            List<ScenePresence> avatars = m_scene.GetAvatars();
            foreach (ScenePresence avatar in avatars)
            {
                if (!avatar.IsChildAgent)
                    avatar.ControllingClient.SendSunPos(Position, Velocity, CurrentTime, SecondsPerSunCycle, SecondsPerYear, OrbitalPosition);
            }

            // set estate settings for region access to sun position
            m_scene.RegionInfo.RegionSettings.SunVector = Position;
            m_scene.RegionInfo.RegionSettings.SunPosition = GetLindenEstateHourFromCurrentTime();
        }

        /// <summary>
        /// Calculate the sun's orbital position and its velocity.
        /// </summary>
        private void GenSunPos()
        {
            TotalDistanceTravelled  = SunSpeed * CurrentTime;  // distance measured in radians
            OrbitalPosition         = (float) (TotalDistanceTravelled%SunCycle); // position measured in radians

            // TotalDistanceTravelled += HoursToRadians-(0.25*Math.PI)*Math.Cos(HoursToRadians)-OrbitalPosition;
            // OrbitalPosition         = (float) (TotalDistanceTravelled%SunCycle);

            SeasonalOffset          = SeasonSpeed * CurrentTime; // Present season determined as total radians travelled around season cycle

            Tilt.W                  = (float) (AverageTilt + (SeasonalTilt*Math.Sin(SeasonalOffset))); // Calculate seasonal orbital N/S tilt

            // m_log.Debug("[SUN] Total distance travelled = "+TotalDistanceTravelled+", present position = "+OrbitalPosition+".");
            // m_log.Debug("[SUN] Total seasonal progress = "+SeasonalOffset+", present tilt = "+Tilt.W+".");

            // The sun rotates about the Z axis

            Position.X = (float) Math.Cos(-TotalDistanceTravelled);
            Position.Y = (float) Math.Sin(-TotalDistanceTravelled);
            Position.Z = 0;

            // For interest we rotate it slightly about the X access.
            // Celestial tilt is a value that ranges .025

            Position *= Tilt;

            // Finally we shift the axis so that more of the
            // circle is above the horizon than below. This
            // makes the nights shorter than the days.

            Position.Z = Position.Z + (float) HorizonShift;
            Position   = Vector3.Normalize(Position);

            // m_log.Debug("[SUN] Position("+Position.X+","+Position.Y+","+Position.Z+")");

            Velocity.X = 0;
            Velocity.Y = 0;
            Velocity.Z = (float) SunSpeed;

            // Correct angular velocity to reflect the seasonal rotation

            Magnitude = Position.Length();
            if (sunFixed)
            {
                Velocity.X = 0;
                Velocity.Y = 0;
                Velocity.Z = 0;
                return;
            }

            Velocity = (Velocity * Tilt) * (1.0f / Magnitude);

            // m_log.Debug("[SUN] Velocity("+Velocity.X+","+Velocity.Y+","+Velocity.Z+")");
        }

        private void ClientLoggedOut(UUID AgentId)
        {
            lock (m_rootAgents)
            {
                if (m_rootAgents.ContainsKey(AgentId))
                {
                    m_rootAgents.Remove(AgentId);
                    m_log.Info("[SUN]: Removing " + AgentId + ". Agent logged out.");
                }
            }
        }

        private void AvatarEnteringParcel(ScenePresence avatar, int localLandID, UUID regionID)
        {
            lock (m_rootAgents)
            {
                if (m_rootAgents.ContainsKey(avatar.UUID))
                {
                    m_rootAgents[avatar.UUID] = avatar.RegionHandle;
                }
                else
                {
                    m_rootAgents.Add(avatar.UUID, avatar.RegionHandle);
                    SunToClient(avatar.ControllingClient);
                }
            }
            //m_log.Info("[FRIEND]: " + avatar.Name + " status:" + (!avatar.IsChildAgent).ToString());
        }

        private void MakeChildAgent(ScenePresence avatar)
        {
            lock (m_rootAgents)
            {
                if (m_rootAgents.ContainsKey(avatar.UUID))
                {
                    if (m_rootAgents[avatar.UUID] == avatar.RegionHandle)
                    {
                        m_rootAgents.Remove(avatar.UUID);
                    }
                }
            }
        }

        public void EstateToolsTimeUpdate(ulong regionHandle, bool FixedTime, bool useEstateTime, float LindenHour)
        {
            if (m_scene.RegionInfo.RegionHandle == regionHandle)
            {
                SetTimeByLindenHour(LindenHour);

                //if (useEstateTime)
                    //LindenHourOffset = 0;

                ForceSunUpdateToAllClients();
                sunFixed = FixedTime;
                if (sunFixed)
                    GenSunPos();
            }
        }
    }
}