"""
Provides a class called DLE to convert and solve dynamic linear economies
(as set out in Hansen & Sargent (2013)) as LQ problems.
"""

import numpy as np
from ._lqcontrol import LQ
from ._matrix_eqn import solve_discrete_lyapunov
from ._rank_nullspace import nullspace

class DLE(object):
    r"""
    This class is for analyzing dynamic linear economies, as set out in Hansen & Sargent (2013).
    The planner's problem is to choose \{c_t, s_t, i_t, h_t, k_t, g_t\}_{t=0}^\infty to maximize

        \max -(1/2) \mathbb{E} \sum_{t=0}^{\infty} \beta^t [(s_t - b_t).(s_t-b_t) + g_t.g_t]

    subject to the linear constraints

        \Phi_c c_t + \Phi_g g_t + \Phi_i i_t = \Gamma k_{t-1} + d_t
        k_t = \Delta_k k_{t-1} + \Theta_k i_t
        h_t = \Delta_h h_{t-1} + \Theta_h c_t
        s_t = \Lambda h_{t-1} + \Pi c_t

    and

        z_{t+1} = A_{22} z_t + C_2 w_{t+1}
        b_t = U_b z_t
        d_t = U_d z_t

    where h_{-1}, k_{-1}, and z_0 are given as initial conditions.

    Section 5.5 of HS2013 describes how to map these matrices into those of
    a LQ problem.

    HS2013 sort the matrices defining the problem into three groups:

    Information: A_{22}, C_2, U_b , and U_d characterize the motion of information
    sets and of taste and technology shocks

    Technology: \Phi_c, \Phi_g, \Phi_i, \Gamma, \Delta_k, and \Theta_k determine the
    technology for producing consumption goods

    Preferences: \Delta_h, \Theta_h, \Lambda, and \Pi determine the technology for
    producing consumption services from consumer goods. A scalar discount factor \beta
    determines the preference ordering over consumption services.

    Parameters
    ----------
    Information : tuple
        Information is a tuple containing the matrices A_{22}, C_2, U_b, and U_d
    Technology : tuple
        Technology is a tuple containing the matrices \Phi_c, \Phi_g, \Phi_i, \Gamma,
        \Delta_k, and \Theta_k
    Preferences : tuple
        Preferences is a tuple containing the scalar \beta and the
        matrices \Lambda, \Pi, \Delta_h, and \Theta_h

    """

    def __init__(self, information, technology, preferences):

        # === Unpack the tuples which define information, technology and preferences === #
        self.a22, self.c2, self.ub, self.ud = information
        self.phic, self.phig, self.phii, self.gamma, self.deltak, self.thetak = technology
        self.beta, self.llambda, self.pih, self.deltah, self.thetah = preferences

        # === Computation of the dimension of the structural parameter matrices === #
        self.nb, self.nh = self.llambda.shape
        self.nd, self.nc = self.phic.shape
        self.nz, self.nw = self.c2.shape
        _, self.ng = self.phig.shape
        self.nk, self.ni = self.thetak.shape

        # === Creation of various useful matrices === #
        uc = np.hstack((np.eye(self.nc), np.zeros((self.nc, self.ng))))
        ug = np.hstack((np.zeros((self.ng, self.nc)), np.eye(self.ng)))
        phiin = np.linalg.inv(np.hstack((self.phic, self.phig)))
        phiinc = uc.dot(phiin)
        b11 = - self.thetah.dot(phiinc).dot(self.phii)
        a1 = self.thetah.dot(phiinc).dot(self.gamma)
        a12 = np.vstack((self.thetah.dot(phiinc).dot(
            self.ud), np.zeros((self.nk, self.nz))))

        # === Creation of the A Matrix for the state transition of the LQ problem === #

        a11 = np.vstack((np.hstack((self.deltah, a1)), np.hstack(
            (np.zeros((self.nk, self.nh)), self.deltak))))
        self.A = np.vstack((np.hstack((a11, a12)), np.hstack(
            (np.zeros((self.nz, self.nk + self.nh)), self.a22))))

        # === Creation of the B Matrix for the state transition of the LQ problem === #

        b1 = np.vstack((b11, self.thetak))
        self.B = np.vstack((b1, np.zeros((self.nz, self.ni))))

        # === Creation of the C Matrix for the state transition of the LQ problem === #

        self.C = np.vstack((np.zeros((self.nk + self.nh, self.nw)), self.c2))

        # === Define R,W and Q for the payoff function of the LQ problem === #

        self.H = np.hstack((self.llambda, self.pih.dot(uc).dot(phiin).dot(self.gamma), self.pih.dot(
            uc).dot(phiin).dot(self.ud) - self.ub, -self.pih.dot(uc).dot(phiin).dot(self.phii)))
        self.G = ug.dot(phiin).dot(
            np.hstack((np.zeros((self.nd, self.nh)), self.gamma, self.ud, -self.phii)))
        self.S = (self.G.T.dot(self.G) + self.H.T.dot(self.H)) / 2

        self.nx = self.nh + self.nk + self.nz
        self.n = self.ni + self.nh + self.nk + self.nz

        self.R = self.S[0:self.nx, 0:self.nx]
        self.W = self.S[self.nx:self.n, 0:self.nx]
        self.Q = self.S[self.nx:self.n, self.nx:self.n]

        # === Use quantecon's LQ code to solve our LQ problem === #

        lq = LQ(self.Q, self.R, self.A, self.B,
                self.C, N=self.W, beta=self.beta)

        self.P, self.F, self.d = lq.stationary_values()

        # === Construct output matrices for our economy using the solution to the LQ problem === #

        self.A0 = self.A - self.B.dot(self.F)

        self.Sh = self.A0[0:self.nh, 0:self.nx]
        self.Sk = self.A0[self.nh:self.nh + self.nk, 0:self.nx]
        self.Sk1 = np.hstack((np.zeros((self.nk, self.nh)), np.eye(
            self.nk), np.zeros((self.nk, self.nz))))
        self.Si = -self.F
        self.Sd = np.hstack((np.zeros((self.nd, self.nh + self.nk)), self.ud))
        self.Sb = np.hstack((np.zeros((self.nb, self.nh + self.nk)), self.ub))
        self.Sc = uc.dot(phiin).dot(-self.phii.dot(self.Si) +
                                    self.gamma.dot(self.Sk1) + self.Sd)
        self.Sg = ug.dot(phiin).dot(-self.phii.dot(self.Si) +
                                    self.gamma.dot(self.Sk1) + self.Sd)
        self.Ss = self.llambda.dot(np.hstack((np.eye(self.nh), np.zeros(
            (self.nh, self.nk + self.nz))))) + self.pih.dot(self.Sc)

        # ===  Calculate eigenvalues of A0 === #
        self.A110 = self.A0[0:self.nh + self.nk, 0:self.nh + self.nk]
        self.endo = np.linalg.eigvals(self.A110)
        self.exo = np.linalg.eigvals(self.a22)

        # === Construct matrices for Lagrange Multipliers === #

        self.Mk = -2 * self.beta.item() * (np.hstack((np.zeros((self.nk, self.nh)), np.eye(
            self.nk), np.zeros((self.nk, self.nz))))).dot(self.P).dot(self.A0)
        self.Mh = -2 * self.beta.item() * (np.hstack((np.eye(self.nh), np.zeros(
            (self.nh, self.nk)), np.zeros((self.nh, self.nz))))).dot(self.P).dot(self.A0)
        self.Ms = -(self.Sb - self.Ss)
        self.Md = -(np.linalg.inv(np.vstack((self.phic.T, self.phig.T))).dot(
            np.vstack((self.thetah.T.dot(self.Mh) + self.pih.T.dot(self.Ms), -self.Sg))))
        self.Mc = -(self.thetah.T.dot(self.Mh) + self.pih.T.dot(self.Ms))
        self.Mi = -(self.thetak.T.dot(self.Mk))

    def compute_steadystate(self, nnc=2):
        """
        Computes the non-stochastic steady-state of the economy.

        Parameters
        ----------
        nnc : array_like(float)
            nnc is the location of the constant in the state vector x_t

        """
        zx = np.eye(self.A0.shape[0])-self.A0
        self.zz = nullspace(zx)
        self.zz /= self.zz[nnc]
        self.css = self.Sc.dot(self.zz)
        self.sss = self.Ss.dot(self.zz)
        self.iss = self.Si.dot(self.zz)
        self.dss = self.Sd.dot(self.zz)
        self.bss = self.Sb.dot(self.zz)
        self.kss = self.Sk.dot(self.zz)
        self.hss = self.Sh.dot(self.zz)

    def compute_sequence(self, x0, ts_length=None, Pay=None):
        """
        Simulate quantities and prices for the economy

        Parameters
        ----------
        x0 : array_like(float)
            The initial state

        ts_length : scalar(int)
            Length of the simulation

        Pay : array_like(float)
            Vector to price an asset whose payout is Pay*xt

        """
        lq = LQ(self.Q, self.R, self.A, self.B,
                self.C, N=self.W, beta=self.beta)
        xp, up, wp = lq.compute_sequence(x0, ts_length)
        self.h = self.Sh.dot(xp)
        self.k = self.Sk.dot(xp)
        self.i = self.Si.dot(xp)
        self.b = self.Sb.dot(xp)
        self.d = self.Sd.dot(xp)
        self.c = self.Sc.dot(xp)
        self.g = self.Sg.dot(xp)
        self.s = self.Ss.dot(xp)

        # === Value of J-period risk-free bonds === #
        # === See p.145: Equation (7.11.2) === #
        e1 = np.zeros((1, self.nc))
        e1[0, 0] = 1
        self.R1_Price = np.empty((ts_length + 1, 1))
        self.R2_Price = np.empty((ts_length + 1, 1))
        self.R5_Price = np.empty((ts_length + 1, 1))
        for i in range(ts_length + 1):
            self.R1_Price[i, 0] = self.beta * e1.dot(self.Mc).dot(np.linalg.matrix_power(
                self.A0, 1)).dot(xp[:, i]) / e1.dot(self.Mc).dot(xp[:, i])
            self.R2_Price[i, 0] = self.beta**2 * e1.dot(self.Mc).dot(
                np.linalg.matrix_power(self.A0, 2)).dot(xp[:, i]) / e1.dot(self.Mc).dot(xp[:, i])
            self.R5_Price[i, 0] = self.beta**5 * e1.dot(self.Mc).dot(
                np.linalg.matrix_power(self.A0, 5)).dot(xp[:, i]) / e1.dot(self.Mc).dot(xp[:, i])

        # === Gross rates of return on 1-period risk-free bonds === #
        self.R1_Gross = 1 / self.R1_Price

        # === Net rates of return on J-period risk-free bonds === #
        # === See p.148: log of gross rate of return, divided by j === #
        self.R1_Net = np.log(1 / self.R1_Price) / 1
        self.R2_Net = np.log(1 / self.R2_Price) / 2
        self.R5_Net = np.log(1 / self.R5_Price) / 5

        # === Value of asset whose payout vector is Pay*xt === #
        # See p.145: Equation (7.11.1)
        if isinstance(Pay, np.ndarray) == True:
            self.Za = Pay.T.dot(self.Mc)
            self.Q = solve_discrete_lyapunov(
                self.A0.T * self.beta**0.5, self.Za)
            self.q = self.beta / (1 - self.beta) * \
                np.trace(self.C.T.dot(self.Q).dot(self.C))
            self.Pay_Price = np.empty((ts_length + 1, 1))
            self.Pay_Gross = np.empty((ts_length + 1, 1))
            self.Pay_Gross[0, 0] = np.nan
            for i in range(ts_length + 1):
                self.Pay_Price[i, 0] = (xp[:, i].T.dot(self.Q).dot(
                    xp[:, i]) + self.q) / e1.dot(self.Mc).dot(xp[:, i])
            for i in range(ts_length):
                self.Pay_Gross[i + 1, 0] = self.Pay_Price[i + 1,
                                                          0] / (self.Pay_Price[i, 0] - Pay.dot(xp[:, i]))
        return

    def irf(self, ts_length=100, shock=None):
        """
        Create Impulse Response Functions

        Parameters
        ----------

        ts_length : scalar(int)
            Number of periods to calculate IRF

        Shock : array_like(float)
            Vector of shocks to calculate IRF to. Default is first element of w

        """

        if type(shock) != np.ndarray:
            # Default is to select first element of w
            shock = np.vstack((np.ones((1, 1)), np.zeros((self.nw - 1, 1))))

        self.c_irf = np.empty((ts_length, self.nc))
        self.s_irf = np.empty((ts_length, self.nb))
        self.i_irf = np.empty((ts_length, self.ni))
        self.k_irf = np.empty((ts_length, self.nk))
        self.h_irf = np.empty((ts_length, self.nh))
        self.g_irf = np.empty((ts_length, self.ng))
        self.d_irf = np.empty((ts_length, self.nd))
        self.b_irf = np.empty((ts_length, self.nb))

        for i in range(ts_length):
            self.c_irf[i, :] = self.Sc.dot(
                np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T
            self.s_irf[i, :] = self.Ss.dot(
                np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T
            self.i_irf[i, :] = self.Si.dot(
                np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T
            self.k_irf[i, :] = self.Sk.dot(
                np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T
            self.h_irf[i, :] = self.Sh.dot(
                np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T
            self.g_irf[i, :] = self.Sg.dot(
                np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T
            self.d_irf[i, :] = self.Sd.dot(
                np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T
            self.b_irf[i, :] = self.Sb.dot(
                np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T

        return

    def canonical(self):
        """
        Compute canonical preference representation
        Uses auxiliary problem of 9.4.2, with the preference shock process reintroduced
        Calculates pihat, llambdahat and ubhat for the equivalent canonical household technology
        """
        Ac1 = np.hstack((self.deltah, np.zeros((self.nh, self.nz))))
        Ac2 = np.hstack((np.zeros((self.nz, self.nh)), self.a22))
        Ac = np.vstack((Ac1, Ac2))
        Bc = np.vstack((self.thetah, np.zeros((self.nz, self.nc))))
        Rc1 = np.hstack((self.llambda.T.dot(self.llambda), -
                         self.llambda.T.dot(self.ub)))
        Rc2 = np.hstack((-self.ub.T.dot(self.llambda), self.ub.T.dot(self.ub)))
        Rc = np.vstack((Rc1, Rc2))
        Qc = self.pih.T.dot(self.pih)
        Nc = np.hstack(
            (self.pih.T.dot(self.llambda), -self.pih.T.dot(self.ub)))

        lq_aux = LQ(Qc, Rc, Ac, Bc, N=Nc, beta=self.beta)

        P1, F1, d1 = lq_aux.stationary_values()

        self.F_b = F1[:, 0:self.nh]
        self.F_f = F1[:, self.nh:]

        self.pihat = np.linalg.cholesky(self.pih.T.dot(
            self.pih) + self.beta.dot(self.thetah.T).dot(P1[0:self.nh, 0:self.nh]).dot(self.thetah)).T
        self.llambdahat = self.pihat.dot(self.F_b)
        self.ubhat = - self.pihat.dot(self.F_f)

        return
