# Copyright 2017 NREL

# Licensed under the Apache License, Version 2.0 (the "License"); you may not use
# this file except in compliance with the License. You may obtain a copy of the
# License at http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.

import numpy as np
from scipy.interpolate import interp1d
from scipy.interpolate import griddata


class Turbine():
    """
    Turbine is model object representing a particular wind turbine. It is largely
    a container of data and parameters, but also contains method to probe properties
    for output.

    inputs:
        instance_dictionary: dict - the input dictionary as generated from the input_reader;
            it should have the following key-value pairs:
                {
        
                    "rotor_diameter": float,
        
                    "hub_height": float,
        
                    "blade_count": int,
        
                    "pP": float,
        
                    "pT": float,
        
                    "generator_efficiency": float,

                    "power_thrust_table": dict,
        
                    "yaw_angle": float,
        
                    "tilt_angle": float,
        
                    "TSR": float
        
                }

    outputs:
        self: Turbine - an instantiated Turbine object
    """

    def __init__(self, instance_dictionary):

        # constants
        self.grid_point_count = 5*5
        if np.sqrt(self.grid_point_count) % 1 != 0.0:
            raise ValueError("Turbine.grid_point_count must be the square of a number")

        self.velocities = [0] * self.grid_point_count

        self.description = instance_dictionary["description"]
        properties = instance_dictionary["properties"]
        self.rotor_diameter = properties["rotor_diameter"]
        self.hub_height = properties["hub_height"]
        self.blade_count = properties["blade_count"]
        self.pP = properties["pP"]
        self.pT = properties["pT"]
        self.generator_efficiency = properties["generator_efficiency"]
        self.power_thrust_table = properties["power_thrust_table"]
        self.yaw_angle = properties["yaw_angle"]
        self.tilt_angle = properties["tilt_angle"]
        self.tsr = properties["TSR"]

        # initialize derived attributes
        self.grid = self._create_swept_area_grid()
        
        # initialize to an invalid value until calculated
        self.air_density = -1

    # Private methods

    def _create_swept_area_grid(self):
        # TODO: add validity check:
        # rotor points has a minimum in order to always include points inside
        # the disk ... 2?
        #
        # the grid consists of the y,z coordinates of the discrete points which
        # lie within the rotor area: [(y1,z1), (y2,z2), ... , (yN, zN)]

        # update:
        # using all the grid point because that how roald did it.
        # are the points outside of the rotor disk used later?

        # determine the dimensions of the square grid
        num_points = int(np.round(np.sqrt(self.grid_point_count)))
        # syntax: np.linspace(min, max, n points)
        horizontal = np.linspace(-self.rotor_radius, self.rotor_radius, num_points)
        vertical = np.linspace(-self.rotor_radius, self.rotor_radius, num_points)

        # build the grid with all of the points
        grid = [(h, vertical[i]) for i in range(num_points) for h in horizontal]

        # keep only the points in the swept area
        grid = [point for point in grid if np.hypot(point[0], point[1]) < self.rotor_radius]

        return grid

    def _fCp(self, at_wind_speed):
        cp = self.power_thrust_table["power"]
        wind_speed = self.power_thrust_table["wind_speed"]
        fCpInterp = interp1d(wind_speed, cp, fill_value='extrapolate')
        if at_wind_speed < min(wind_speed):
            return max(cp)
        else:
            _cp = fCpInterp(at_wind_speed)
            if _cp.size > 1:
                _cp = _cp[0]
            return float(_cp)

    def _fCt(self, at_wind_speed):
        ct = self.power_thrust_table["thrust"]
        wind_speed = self.power_thrust_table["wind_speed"]
        fCtInterp = interp1d(wind_speed, ct, fill_value='extrapolate')
        if at_wind_speed < min(wind_speed):
            return 0.99
        else:
            _ct = fCtInterp(at_wind_speed)
            if _ct.size > 1:
                _ct = _ct[0]
            return float(_ct)

    def _calculate_swept_area_velocities(self, wind_direction, local_wind_speed, coord, x, y, z):
        """
        Initialize the turbine disk velocities used in the 3D model based on shear using the power log law.
        """
        u_at_turbine = local_wind_speed
        x_grid = x
        y_grid = y
        z_grid = z

        yPts = np.array([point[0] for point in self.grid])
        zPts = np.array([point[1] for point in self.grid])

        # interpolate from the flow field to get the flow field at the grid points
        dist = [np.sqrt((coord.x1 - x_grid)**2 + (coord.x2 + yPts[i] - y_grid)**2 + (self.hub_height + zPts[i] - z_grid)**2) for i in range(len(yPts))]
        idx = [np.where(dist[i] == np.min(dist[i])) for i in range(len(yPts))]
        data = [np.mean(u_at_turbine[idx[i]]) for i in range(len(yPts))]
        return np.array(data)

    # Public methods

    def calculate_turbulence_intensity(self, flow_field_ti, velocity_model, turbine_coord, wake_coord, turbine_wake):
        """
        flow_field_ti
        velocity_model
        turbine_coord
        wake_coord
        turbine_wake
        """

        ti_initial = flow_field_ti

        # user-input turbulence intensity parameters
        ti_i = velocity_model.ti_initial
        ti_constant = velocity_model.ti_constant
        ti_ai = velocity_model.ti_ai
        ti_downstream = velocity_model.ti_downstream

        # turbulence intensity calculation based on Crespo et. al.
        ti_calculation = ti_constant \
                       * turbine_wake.aI**ti_ai \
                       * ti_initial**ti_i \
                       * ((turbine_coord.x1 - wake_coord.x1) / self.rotor_diameter)**ti_downstream

        return np.sqrt(ti_calculation**2 + flow_field_ti**2)

    def update_velocities(self, u_wake, coord, flow_field, rotated_x, rotated_y, rotated_z):
        """
        """
        # reset the waked velocities
        local_wind_speed = flow_field.u_initial - u_wake
        self.velocities = self._calculate_swept_area_velocities(
            flow_field.wind_direction,
            local_wind_speed,
            coord,
            rotated_x,
            rotated_y,
            rotated_z
        )

    # Getters & Setters
    @property
    def rotor_radius(self):
        return self.rotor_diameter / 2.0

    @property
    def yaw_angle(self):
        return self._yaw_angle

    @yaw_angle.setter
    def yaw_angle(self, value):
        self._yaw_angle = value

    @property
    def tilt_angle(self):
        return self._tilt_angle

    @tilt_angle.setter
    def tilt_angle(self, value):
        self._tilt_angle = value

    @property
    def average_velocity(self):
        return np.cbrt(np.mean(self.velocities**3))

    @property
    def Cp(self):
        return self._fCp(self.average_velocity)

    @property
    def Ct(self):
        return self._fCt(self.average_velocity) * np.cos(self.yaw_angle)**self.pP

    @property
    def power(self):
        cptmp = self.Cp \
                * np.cos(self.yaw_angle)**self.pP \
                * np.cos(self.tilt_angle)**self.pT
        return 0.5 * self.air_density * (np.pi * self.rotor_radius**2) \
                * cptmp * self.generator_efficiency \
                * self.average_velocity**3

    @property
    def aI(self):
        return 0.5 / np.cos(self.yaw_angle) \
            * (1 - np.sqrt(1 - self.Ct * np.cos(self.yaw_angle)))