package com.daragallagher.finance;

import java.awt.KeyboardFocusManager;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JButton;
import javax.swing.JSeparator;
import javax.swing.JComboBox;
import javax.swing.JTextField;
import javax.swing.JComponent;
import javax.swing.UIManager;

/**
 * This applet calculates the real cost of credit of credit union
 * borrowing using the formula in the fourth schedule of the 1995
 * credit consumer act.
 * <p>
 *
 * Copyright 2005 Dara Gallagher
 * 
 *    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.
 */
public class CreditUnionAprApplet
extends javax.swing.JApplet
{
    public CreditUnionAprApplet()
    {
        // Attempt to try to get Windows L&F instead of the horrible
        // default Swing one.
        try
        {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        }
        catch (Exception e) { e.printStackTrace(); }
    }

    public void init()
    {
        setLayout(null);
        add(new CreditUnionAprPanel());
    }

    /**
     * Also works as a standalone Java application.
     */
    public static void main(String[] args)
    {
        new CreditUnionAprPanel();
        JFrame frame = new JFrame();
        frame.setLayout(null);
        frame.add(new CreditUnionAprPanel());
        frame.setVisible(true);
    }
}

/**
 * The calculator GUI panel.  Contains the labels, fields for values and
 * the underlying Loan business object.
 */
class CreditUnionAprPanel extends JPanel
implements java.awt.event.FocusListener, java.awt.event.ActionListener
{
    private Loan loan = new Loan();

    // fields:
    private FrequencyField periodicityField = new FrequencyField();
    private IntegerField periodsField = new IntegerField();
    private AmountField savingsAmountField = new AmountField();
    private RateField savingsRateField = new RateField();
    private AmountField loanAmountField = new AmountField();
    private RateField repaymentRateField = new RateField();
    private AmountField repaymentAmountField = new AmountField();
    private AmountField savingsAtEndField = new AmountField();
    private RateField aprField = new RateField();

    // positioning constants:
    private static final int WIDTH = 400;
    private static final int FIELD_X = 200;
    private static final int FIELD_WIDTH = 100;
    private static final int FIELD_HEIGHT = 20;
    private static final int BORDER = 5;

    private int y = BORDER;

    public CreditUnionAprPanel()
    {
        // Layout managers are more bother than they're worth.
        setLayout(null);

        // These fields are for output only:
        aprField.setEditable(false);
        aprField.setFocusable(false);
        savingsAtEndField.setEditable(false);
        savingsAtEndField.setFocusable(false);
        repaymentAmountField.setEditable(false);
        repaymentAmountField.setFocusable(false);

        // Add the fields to this panel:
        addField("Frequency of Repayments", periodicityField);
        addField("Number of Repayments", periodsField);
        addField("Loan Amount", loanAmountField);
        addField("Loan Rate", repaymentRateField);
        addField("Repayment Amount", repaymentAmountField);
        addDivider();
        addField("Manditory Savings Amount", savingsAmountField);
        addField("Savings Rate", savingsRateField);
        addField("Savings At End", savingsAtEndField);
        addDivider();
        addField("Overall APR", aprField);

        setBounds(0, 0, WIDTH, y + BORDER); 

        // refresh the contents of the fields from the Loan object:
        refreshFields();
    }

    private void addField(String description, JComponent editComponent)
    {
        JLabel label = new JLabel(description);
        add(label);
        int width = (int)label.getPreferredSize().getWidth();
        label.setBounds(FIELD_X - BORDER - width, y, width, FIELD_HEIGHT);

        add(editComponent);
        editComponent.setBounds(FIELD_X, y, FIELD_WIDTH, FIELD_HEIGHT);

        if (editComponent instanceof JTextField)
            editComponent.addFocusListener(this);
        else if (editComponent instanceof JComboBox)
            ((JComboBox)editComponent).addActionListener(this);

        y = y + FIELD_HEIGHT + BORDER;
    }

    private void addDivider()
    {
        JSeparator sep = new JSeparator();
        sep.setBounds(BORDER, y, WIDTH - (2 * BORDER), BORDER);
        add(sep);

        y = y + (2 * BORDER);
    }

    public void actionPerformed(java.awt.event.ActionEvent e)
    {
        updateLoanParameter(periodicityField);
    }

    public void focusGained(java.awt.event.FocusEvent e)
    {
        java.awt.Component field = e.getComponent();
       
        if (field instanceof JTextField)
            ((JTextField)field).selectAll();
    }

    public void focusLost(java.awt.event.FocusEvent e)
    {
        // update the underlying Loan object with the value of the field
        // we've just left:
        updateLoanParameter((JComponent)e.getComponent());
    }
    
    private void updateLoanParameter(JComponent field)
    {
        try
        {
            if (field == periodicityField)
                loan.setPeriodsPerYear(periodicityField.getValue());
            else if (field == periodsField)
                loan.setPeriods(periodsField.getValue());
            else if (field == savingsAmountField)
                loan.setSavingsAmount(savingsAmountField.getValue());
            else if (field == savingsRateField)
                loan.setSavingsRate(savingsRateField.getValue());
            else if (field == loanAmountField)
                loan.setLoanAmount(loanAmountField.getValue());
            else if (field == repaymentRateField)
                loan.setRepaymentRate(repaymentRateField.getValue());
        }
        catch (Loan.Exception ex)
        {
            javax.swing.JOptionPane.showMessageDialog(this, ex.getMessage());
            loan.reset();
        }

        // refresh the contents of the fields from the Loan object:
        refreshFields();
    }

    private void refreshFields()
    {
        periodicityField.setValue(loan.getPeriodsPerYear());
        periodsField.setValue(loan.getPeriods());
        savingsAmountField.setValue(loan.getSavingsAmount());
        savingsRateField.setValue(loan.getSavingsRate());
        loanAmountField.setValue(loan.getLoanAmount());
        repaymentRateField.setValue(loan.getRepaymentRate());
        repaymentAmountField.setValue(loan.getRepaymentAmount());
        savingsAtEndField.setValue(loan.getSavingsAtEnd());
        aprField.setValue(loan.getApr());
    }
}

class IntegerField extends JTextField
{
    IntegerField()
    {
        setSize(120, 20);
    }

    public void setValue(int i)
    {
        setText(Integer.toString(i));
    }

    public int getValue()
    {
        try
        {
            return Integer.parseInt(getText());
        }
        catch (NumberFormatException e)
        {
            return 0;
        }
    }
}

class AmountField extends JTextField
{
    AmountField()
    {
        setSize(120, 20);
    }

    public void setValue(double amount)
    {
        setText(String.format("%.2f", amount));
    }

    public double getValue()
    {
        try
        {
            double amt = Double.parseDouble(getText());
            return (double)Math.round(amt * 100) / 100.0;
        }
        catch (NumberFormatException e) { return 0.0; }
    }
}

class RateField extends JTextField
{
    RateField()
    {
        setSize(200, 20);
    }

    public void setValue(double rate)
    {
        setText(String.format("%2.1f%%", rate * 100));
    }

    public double getValue()
    {
        String amt = getText().trim();
        double factor = 1;
        if (amt.endsWith("%"))
        {
            amt = amt.substring(0, amt.length() - 1);
            factor = .01;
        }
        try { return Double.parseDouble(amt) * factor; }
        catch (NumberFormatException e) { return 0.0; }
    }
}

class FrequencyField extends JComboBox
{
    FrequencyField()
    {
        addItem("Weekly");
        addItem("Fortnightly");
        addItem("Monthly");
        addItem("Quarterly");
        addItem("Yearly");
        setEditable(false);
    }

    public void setValue(int periodsPerYear)
    {
        switch (periodsPerYear)
        {
            case 52:
                setSelectedItem("Weekly");
                break;
            case 26:
                setSelectedItem("Fortnightly");
                break;
            case 12:
                setSelectedItem("Monthly");
                break;
            case 4:
                setSelectedItem("Quarterly");
                break;
            case 1:
                setSelectedItem("Yearly");
                break;
            default:
                throw new RuntimeException("illegal number of periods");
        }
    }

    public int getValue()
    {
        Object o = getSelectedItem();

        if (o.equals("Weekly")) return 52;
        else if (o.equals("Fortnightly")) return 26;
        else if (o.equals("Monthly")) return 12;
        else if (o.equals("Quarterly")) return 4;
        else if (o.equals("Yearly")) return 1;
        else throw new RuntimeException("unexpecte value " + o + " in combo");
    }
}

/**
 * This class contains the real business logic.
 */
class Loan
{
    // inputs
    private int periodsPerYear;
    private int periods;
    private double savingsAmount;
    private double savingsRate;
    private double loanAmount;
    private double repaymentRate;

    // these are derived:
    private double repaymentAmount;
    private double savingsAtEnd;
    private double apr;

    public Loan()
    {
        reset();
    }
    
    public String toString()
    {
        StringBuffer buf = new StringBuffer("LOAN{"
            + "periodsPerYear = " + periodsPerYear
            + ", periods = " + periods
            + ", savingsAmount = " + savingsAmount
            + ", savingsRate = " + savingsRate
            + ", loanAmount = " + loanAmount
            + ", repaymentRate = " + repaymentRate
            + ", repaymentAmount = " + repaymentAmount
            + ", savingsAtEnd = " + savingsAtEnd
            + ", apr = " + apr
            + ", pv(savingsAtEnd) = " + pv(savingsAtEnd, apr, periods)
            + ", pv(payments) = " + pv(repaymentAmount, apr, 1));

        for (int t = 2; t <= periods; t++)
        {
            buf.append(",");
            buf.append(pv(repaymentAmount, apr, t));
        }

        buf.append("}");

        return buf.toString();
    }

    public void reset()
    {
        periodsPerYear = 12;
        periods = 48;
        savingsAmount = 1000;
        savingsRate = 0.03;
        loanAmount = 4000;
        repaymentRate = 0.08;
        apr = 0.0;

        recalc();
    }

    public int getPeriodsPerYear() { return periodsPerYear; }
    public int getPeriods() { return periods; }
    public double getSavingsAmount() { return savingsAmount; }
    public double getSavingsRate() { return savingsRate; }
    public double getLoanAmount() { return loanAmount; }
    public double getRepaymentRate() { return repaymentRate; }

    public double getRepaymentAmount() { return repaymentAmount; }
    public double getSavingsAtEnd() { return savingsAtEnd; }
    public double getApr() { return apr; }

    public void setRepaymentRate(double rate)
    {
        repaymentRate = rate;
        recalc();
    }

    public void setPeriodsPerYear(int x)
    {
        // we try to keep the duration of the loan the same:
        periods = periods * x / periodsPerYear;
        periodsPerYear = x;
        recalc();
    }

    public void setPeriods(int x)
    {
        periods = x;
        recalc();
    }

    public void setSavingsAmount(double x)
    {
        savingsAmount = x;
        recalc();
    }

    public void setSavingsRate(double x)
    {
        savingsRate = x;
        recalc();
    }

    public void setLoanAmount(double x)
    {
        loanAmount = x;
        recalc();
    }

    private double compoundedRate(double apr, int periods)
    {
        return Math.pow(1 + apr, (double)periods / (double)periodsPerYear);
    }

    /** Future value function. */
    private double fv(double amount, double apr, int periods)
    {
        return amount * compoundedRate(apr, periods);
    }

    /** Present value function. */
    private double pv(double amount, double apr, int periods)
    {
        return amount / compoundedRate(apr, periods);
    }

    /** Payment function. */
    private double pmt(double amount, double apr, int periods)
    {
        double periodRate = 1.0 / Math.pow(1.0 + apr, 1.0 / periodsPerYear);

        return (amount * (periodRate - 1.0)) /
                (Math.pow(periodRate, periods + 1.0) - periodRate);
    }

    /**
     * Calculates the total present value of the credit union
     * arrangement as a function of the APR.  We are trying to find the
     * root of this function - i.e. the APR which makes the total
     * present value equal zero.
     */
    private double f(double apr)
    {
        double total = loanAmount + pv(savingsAtEnd, apr, periods);
        total = total - savingsAmount;

        for (int t = 1; t <= periods; t++)
        {
            total = total - pv(repaymentAmount, apr, t);
        }

        return total;
    }

    public static class Exception extends RuntimeException
    {
        Exception()
        {
            super("Cannot calculate APRs which are negative or larger than 100%");
        }
    }

    private void recalc()
    {
        // do the easy to derive stuff first:
        savingsAtEnd = fv(savingsAmount, savingsRate, periods);
        repaymentAmount = pmt(loanAmount, repaymentRate, periods);

        // bisection method of approximation:
        double a = 0.0;
        double b = 100.1;

        while (Math.abs(a - b) > 0.000001)
        {
            double f_a = f(a);
            double f_b = f(b);
            if (f_a == 0.0) break;
            if (f_b == 0.0)
            {
                a = b;
                break;
            }
            if (f_a * f_b > 0.0)
            {
                System.out.println("error getting apr for " + this);
                throw new Exception();
            }

            double mid = (a + b) / 2.0;
            double f_mid = f(mid);
            if (f_a * f_mid < 0.0)
            {
                b = mid;
            }
            else
            {
                a = mid;
            }
            // we should cache f_mid for the next iteration.
        }

        apr = a;
    }
}
