function [ est ] = panel( id, time, y, X, method, varargin )
%PANEL Panel data estimation
%   Computes panel data estimation for fixed effects, between, and random
%   effects.
%
%   est = PANEL(id, time, y, X, method) computes a panel data estimation of
%   y into X with the specified method, where id and time are the individual
%   and time identifiers. The function returns an estimation ouput structure,
%   estout.
%   est = PANEL(id, time, y, X, method, Name,Value) computes panel data 
%   estimation with additional properties using one or more Name,Value pair
%   arguments.
%
%   method is one of the following:
%   - 'po': pooling estimation
%   - 'fe': fixed effects (whitin) estimation
%   - 'be': between estimation
%   - 're': random effects GLS estimation
%
%   Additional properties:
%   - 'vartype': compute heteroskedasticity 'robust' or 'cluster' variance 
%   matrix estiamtion. Default 'homo' for homoskedasticity.
%   - 'clusterid': specify the cluster variable. Only if 'vartype' is set
%   to 'cluster'. Default equal to id.
%   - 'dfcorrection': 0 to supress degrees-of-freedom correction of the
%   variance. Default 1.
%
%   Example
%     
%      est = panel(id, time, y, X, 'fe');
%      est = panel(id, time, y, X, 'be');
%      est = panel(id, time, y, X, 're');
%
%   See also ESTOUT, OLS, IVPANEL, SPANEL
%
%   Copyright 2013-2015 Inmaculada C. Álvarez, Javier Barbero, José L. Zofío
%   http://www.paneldatatoolbox.com
%
%   Version: 2.0
%   LAST UPDATE: 16, June, 2016
%

    % Create output structure
    est = estout();
    
    % Check input
    if nargin < 3
        error('Dependent variable not specified');
    end
    if nargin < 4
        error('Independent variables not specified');
    end
    if size(y,2) ~= 1
        error('Y must be a column vector of the dependant variable');
    end
    if size(y,1) ~= size(X,1)
        error('Number of rows in Y must be equal to number of rows in X');
    end
    if size(id,1) ~= size(y,1)
        error('Number of rows in id must be equal to number of rows in Y');
    end
    if size(time,1) ~= size(y,1)
        error('Number of rows in time must be equal to number of rows in Y');
    end
    if nargin < 5
        error('Panel method not specified')
    end   
        
    % Parse Additional options
    p = inputParser;
    if verLessThan('matlab', '8.2')
        addPar = @(v1,v2,v3,v4) addParamValue(v1,v2,v3,v4);
    else
        addPar = @(v1,v2,v3,v4) addParameter(v1,v2,v3,v4);
    end
    addPar(p,'vartype','homo',...
                 @(x) any(validatestring(x,{'homo','robust','cluster'})));
    addPar(p,'dfcorrection',1,@(x) isnumeric(x));
    addPar(p,'clusterid',id,@(x) length(x) == length(y))
    p.parse(varargin{:})
    options = p.Results;
    
    % Extract table names and convert data to array
    [y, ynames] = extracttable(y);
    [X, xnames] = extracttable(X);  
    
    % Error if NaN's in input data
    if any(isnan(id)) ||any(isnan(time)) || any(isnan(y)) || any(any(isnan(X))) || any(any(isnan(options.clusterid)))
        error('NaN values not allowed in input data');
    end
    
    % Check method
    if ~ismember(method,{'po','fe','be','re'})
        error('Incorrect method')
    end
             
    % Automatically change 'robust' to 'cluster'
    if strcmp(options.vartype,'robust') 
        options.vartype = 'cluster';        
    end
    
    % Check if robust
    if strcmp(options.vartype,'cluster')
        robust = 1;
        
        if strcmp(method,'be')
            error('Robust standard errors not available for between estimation');
        end
    else
        robust = 0;
    end
    
    % Get number of observations
    N = size(y,1);         
    
    % Get balanced and T variables
    [ isBalanced, idx, n, T, Tid, Tmean, Thmean] = isbalancedpanel( id, time );
    
    % Sort variables
    id = id(idx);
    time = time(idx);
    y = y(idx,:);
    X = X(idx,:);
    options.clusterid = options.clusterid(idx); % Sort clusterid
    
    % Store original data (sorted)
    est.id = id;
    est.uid = unique(id);
    est.time = time;
    est.idx = idx;
    est.y = y;
    est.X = X;
               
    % Number of varaibles and hasConstant
    k = size(X,2);
    if ~strcmp(method,'fe')
        k = k + 1;  % For the constant term in non fe estiamtions
        hasConstant = 1;
    else
        hasConstant = 0;
    end
    
    % Transform variables
    switch method
        case 'po'
            % Add constant term
            X = [X ones(N,1)];
            
            % No transformation por pool
            ytr = y;
            Xtr = X;
            
            resdf = N-k;
        case 'fe'      
            % Fixed effects transformation
            ytr = y - groupmeans(id,y,'replicate',1);
            Xtr = X - groupmeans(id,X,'replicate',1);
            
            if ~robust
                resdf = (N-n)-k; % (N-n) instead of (n*T) for compatibility with unbalanced panels
            else
                resdf = n-1;
            end

        case 'be'
            % Add constant term
            X = [X ones(N,1)];
            
            % Between transformation.
            ytr = groupmeans(id, y);
            Xtr = groupmeans(id, X);
            
            resdf = n-k;
        case 're'
            % Get time invariant variables to remove from the first 'fe'
            % and 'be' regressions
            tinvariant = istinvariant(id,X);
            
            % Get sigma2_v from 'fe' residuals
            estfe = panel(id,time,y,X(:,~tinvariant),'fe');
            sigma2_v = estfe.resvar;
            
            % Get sigma2_1 from 'be' residuals
            estbe = panel(id,time,y,X(:,~tinvariant),'be');
            sigma2_1 = estbe.resvar;
            
            % sigma2_u and rho_var
            if isBalanced
                % Calculate sigma2_mu
                sigma2_mu = (sigma2_1 - sigma2_v/T);
            else                                
                % Calculate sigma2_mu using the harmonic mean of T
                sigma2_mu = (sigma2_1 - sigma2_v/Thmean);
            end
            % correct sigma2_mu if < 0
            if sigma2_mu <= 0
                sigma2_mu = 0;
            end
            rho_mu = sigma2_mu / (sigma2_mu + sigma2_v);
            
            % Theta
            if isBalanced
                theta = 1 - sqrt(sigma2_v./(T*sigma2_mu + sigma2_v));
            else
                % Tid_g = repelem(Tid,Tid,1);
                Tid_g = replicate_elements(Tid,Tid);
                theta = 1 - sqrt(sigma2_v./(Tid_g.*sigma2_mu + sigma2_v));
            end
            
            % Add constant term
            X = [X ones(N,1)];
            
            % Random effects transformation
            if isBalanced
                ytr = y - theta * groupmeans(id,y,'replicate',1);            
                Xtr = X - theta * groupmeans(id,X,'replicate',1);
            else
                ytr = y - repmat(theta,1,N) * groupmeans(id,y,'replicate',1);            
                Xtr = X - repmat(theta,1,N) * groupmeans(id,X,'replicate',1);
            end
            
            resdf = N-k;
                       
    end
    
    % Compute estimates
    coef = (Xtr'*Xtr)\Xtr'*ytr;
    
    % Fitted values
    yhattr = Xtr*coef;
    yhat = X*coef;
    
    % Residuals
    res = ytr - yhattr;
        
    % Inverse of Xtr'Xtr
    invXtrXtr = ((Xtr'*Xtr)\eye(k));
    
    % Residual variance
    resvar = (res'*res) ./ resdf;
    
    % Covariance matrix
    switch options.vartype
        case 'homo'
            varcoef = resvar * invXtrXtr;
        case {'robust','cluster'}
            % White-robust standard errors adjusted for cluster
            % Get unique ids
            id_clus = options.clusterid;
            uid_clus = unique(id_clus);
            n_clus = length(uid_clus);
            
            % Compute total sum
            total_sum = 0;
            for i=1:1:n_clus;
                total_sum = total_sum + Xtr(id_clus==uid_clus(i),:)'*res(id_clus==uid_clus(i))*res(id_clus==uid_clus(i))'*Xtr(id_clus==uid_clus(i),:);
            end
            varcoef = invXtrXtr * total_sum * invXtrXtr; 
              
            % Degrees of freedom correction
            if options.dfcorrection
                if strcmp(method,'fe')
                    varcoef = (n_clus/(n_clus-1)) * ((N)/(N-k)) .* varcoef;
                else
                    varcoef = (n_clus/(n_clus-1)) * ((N-1)/(N-k)) .* varcoef;
                end
            end
            
            % Change resdf to the correct value with cluster
            resdf = n_clus - 1;
    end

    % Standard errors
    stderr = sqrt(diag(varcoef));
    
    % Goodness of fit
    if strcmp(method,'be')
        M0 = eye(n) - 1/n * ones(n);
        adjr2_correction = (n - 1) ./ (resdf);
    else
        M0 = eye(N) - 1/N * ones(N);
        if strcmp(method,'fe') && robust
            adjr2_correction = (N - 1) ./ ((N-n)-k); 
        else
            adjr2_correction = (N - 1) ./ (resdf);
        end
    end
    RSS = res' * res;
    TSS = y' * y;
    ESS = TSS - RSS;
    r2 = 1 - (res' * M0 * res) ./ (ytr' * M0 * ytr);
    adjr2 = 1 - adjr2_correction .* (1 - r2);

    % Compute correc r2 for 're'
    if strcmp(method,'re')
        temp = corrcoef(yhat,y);
        r2 = temp(1,2).^2;
        adjr2 = 1 - adjr2_correction .* (1 - r2);
    end
    
    % Save estimation
    est.method = upper(method);
    est.options = options;
    est.options.method = method;
    est.n = n;
    est.T = T;
    est.N = N;
    est.k = k;
    est.isPanel = 1;
    est.isBalanced = isBalanced;
    est.isMultiEq = 0;
    est.isLinear = 1;
    est.hasConstant = hasConstant;
    est.isRobust = robust;
    est.coef = coef;
    est.varcoef = varcoef;
    est.stderr = stderr;
    est.yhat = yhat;
    est.yhattr = yhattr;
    est.res = res;
    est.resvar = resvar;
    est.resdf = resdf;
    
    est.Tid = Tid;
    est.Tmean = Tmean;
    est.Thmean = Thmean;
    if strcmp(method,'re')
        est.theta = theta;
        est.sigma2_mu = sigma2_mu;
        est.sigma2_1 = sigma2_1;
        est.sigma2_v = sigma2_v;
        est.rho_mu = rho_mu;
    end
    
    est.RSS = RSS;
    est.ESS = ESS;
    est.TSS = TSS;
    est.r2 = r2;
    est.adjr2 = adjr2;    
    if strcmp(method,'re')
        est.isAsymptotic = 1;
    else
        est.isAsymptotic = 0;
    end
    est.vartype = options.vartype;
            
    % Set default var names
    est.ynames = ynames;
    est.xnames = xnames;
    est = defaultVarNames(est);
    
end

% OLD CODE: 
% Usefull matrices
    %{
    if isBalanced
        Jbar = ones(T)./T;
        P = kron(eye(n),Jbar);  % Group means and replicate
        Q = eye(N) - P;         % Difference from group means
        Tid_g = (T*ones(N,1));  % Time periods per id, repeated for all observations
    else
        P = zeros(N);
        Tid_g = zeros(N,1);
        lastt=1;
        for i=1:n
            Jbar = ones(Tid(i))./Tid(i);
            P(lastt:lastt+Tid(i)-1, lastt:lastt+Tid(i)-1) = Jbar;
            Tid_g(lastt:lastt+Tid(i)-1,1) = Tid(i);
            lastt = lastt + Tid(i);
        end
        Q = eye(N) - P;
    end  
    %}

        %{
        case 'fe':
            ytr = Q*y;
            Xtr = Q*X;
        %}

        %{
        case 're':
            if isBalanced
                Qre = eye(N) - theta * P;
            else
                Qre = eye(N) - repmat(theta,1,N) .* P;
            end
            ytr = Qre * y;
            Xtr = Qre * X;   

        %}
