/***************************************************************************
 *   Copyright (C) 2005 by Thierry CHARLES   *
 *   thierry@les-charles.net   *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/
#include "tcalculator.h"

#include <ctype.h>
#include <math.h>
#include <stdlib.h>
#include <wx/sizer.h>
#include <wx/colour.h>
#include <wx/msgdlg.h>
#include <wx/clipbrd.h>

#include "lib/lib_logging.h"

IMPLEMENT_DYNAMIC_CLASS(TCalculator,TPanel);

BEGIN_EVENT_TABLE(TCalculator,TPanel)
END_EVENT_TABLE()

const char szCarre_TCALCULATOR[] = {(char)0xB2,(char)0};



TCalculator::TCalculator(wxWindow * parent)
    : TPanel(parent),
        display(NULL)
{
    wxBoxSizer * sizer = new wxBoxSizer( wxVERTICAL);
    this->SetSizer(sizer);
    
    TPanel * displayPanel = new TPanel(this, wxSUNKEN_BORDER);
    wxBoxSizer * displaySizer = new wxBoxSizer(wxVERTICAL);
    displayPanel->SetSizer(displaySizer);
    this->display = new TLabel(displayPanel, wxWANTS_CHARS | wxTAB_TRAVERSAL);
    this->display->setText(wxT("0"));
    this->display->setAlignment(TLabel::Right);
    this->display->SetBackgroundColour(wxColour(255,255,204));
    displaySizer->Add(this->display, 0, wxEXPAND);
    sizer->Add(displayPanel, 0, wxEXPAND);
    this->display->Connect( wxEVT_KEY_DOWN, wxKeyEventHandler(TCalculator::onKeyEvent), NULL, this );
    this->display->Connect( wxEVT_CHAR, wxKeyEventHandler(TCalculator::onKeyEvent), NULL, this );
    this->display->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler(TCalculator::onRightClick), NULL, this );
    
    TPanel * btnsPanel = new TPanel(this);
    wxGridSizer * btnsSizer = new wxGridSizer(5,5,2,2);
    btnsPanel->SetSizer(btnsSizer);
    
    this->buildButton(btnsPanel,ISO2WX(szCarre_TCALCULATOR), *wxBLUE);
    this->buildButton(btnsPanel,wxT("Sqrt"), *wxBLUE);
    this->buildButton(btnsPanel,wxT("Cos"), *wxBLUE);
    this->buildButton(btnsPanel,wxT("Sin"), *wxBLUE);
    this->buildButton(btnsPanel,wxT("Tan"), *wxBLUE);
    
    this->buildButton(btnsPanel,wxT("7"), *wxGREEN);
    this->buildButton(btnsPanel,wxT("8"), *wxGREEN);
    this->buildButton(btnsPanel,wxT("9"), *wxGREEN);
    this->buildButton(btnsPanel,wxT("+"), *wxBLUE);
    this->buildButton(btnsPanel,wxT("("), *wxBLUE);
    
    this->buildButton(btnsPanel,wxT("4"), *wxGREEN);
    this->buildButton(btnsPanel,wxT("5"), *wxGREEN);
    this->buildButton(btnsPanel,wxT("6"), *wxGREEN);
    this->buildButton(btnsPanel,wxT("-"), *wxBLUE);
    this->buildButton(btnsPanel,wxT(")"), *wxBLUE);
    
    this->buildButton(btnsPanel,wxT("1"), *wxGREEN);
    this->buildButton(btnsPanel,wxT("2"), *wxGREEN);
    this->buildButton(btnsPanel,wxT("3"), *wxGREEN);
    this->buildButton(btnsPanel,wxT("*"), *wxBLUE);
    this->buildButton(btnsPanel,wxT("Clr"), *wxRED, wxTr("Delete"));
    
    this->buildButton(btnsPanel,wxT("0"), *wxGREEN);
    this->buildButton(btnsPanel,wxT("."), *wxGREEN);
    this->buildButton(btnsPanel,wxT("="), *wxRED);
    this->buildButton(btnsPanel,wxT("/"), *wxBLUE);
    this->buildButton(btnsPanel,wxT("<-"), *wxRED, wxTr("Backspace"));
    
    sizer->Add(btnsPanel,0, wxEXPAND);
}

TCalculator::~TCalculator()
{
}

void TCalculator::buildButton(TPanel * panel, const wxString & sTxt, const wxColour & colour, const wxString & sTooltip)
{
    TGenButton * btn = new TGenButton(panel);
    btn->setText(sTxt);
    if(sTooltip.Length())
        btn->SetToolTip(sTooltip);
    btn->addButtonListener(this);
    panel->GetSizer()->Add(btn,0, wxEXPAND);
    btn->Connect( wxEVT_KEY_DOWN, wxKeyEventHandler(TCalculator::onKeyEvent), NULL, this );
    btn->Connect( wxEVT_CHAR, wxKeyEventHandler(TCalculator::onKeyEvent), NULL, this );
    
    wxColour btncolour(colour.Red()/8 + 204,
                        colour.Green()/8 + 204,
                        colour.Blue()/8 + 204);
    btn->SetBackgroundColour(btncolour);
}

/** un bouton a ete active */
void TCalculator::buttonActivated(TGenButton * btn)
{
    if(btn)
    {
        this->perform(btn->getText());
    }
}

wxMenu * TCalculator::getPopupMenu()
{
    this->buildMenu();
    return this->rightClickMenu;
}

void TCalculator::buildMenu()
{
    if(this->rightClickMenu)
        return;

    this->display->Connect( wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(TCalculator::onMenuClick), NULL, this );

    this->rightClickMenu = new wxMenu();
    this->rightClickMenu->Append( Menu_Copy, wxTr( "Copy" ) );
}

void TCalculator::onMenuClick(wxCommandEvent& evt)
{
    switch(evt.GetId())
    {
        case Menu_Copy:
        {
            wxString sSelection = this->display->getText();
            if(sSelection.length())
            {
                wxTheClipboard->UsePrimarySelection(false);
                if (wxTheClipboard->Open())
                {
                    wxTheClipboard->Clear();
                    wxTheClipboard->AddData( new wxTextDataObject(sSelection) );
                    wxTheClipboard->Close();
                }
            }
            break;
        }
        default:
            break;
    }
}

void TCalculator::onRightClick(wxMouseEvent & _mouseEvent)
{
    this->display->PopupMenu(this->getPopupMenu(),_mouseEvent.GetPosition());
}

void TCalculator::onKeyEvent(wxKeyEvent & _e)
{
    if(_e.ControlDown() || _e.AltDown() || _e.CmdDown())
    {
        _e.Skip();
        return;
    }

    int iKeyCode = _e.GetKeyCode();
    if(_e.GetEventType() == wxEVT_CHAR)
    {
        if(isdigit(iKeyCode) || iKeyCode == '(' || iKeyCode == ')' || iKeyCode == '^'
            || iKeyCode == '/' || iKeyCode == '*' || iKeyCode == '-' || iKeyCode == '+' || iKeyCode == '.')
        {
            const char sz[] = {iKeyCode,0};
            this->perform( ISO2WX(sz) );
        }
        else if (iKeyCode == 0xB2)
        {
            if(_e.ShiftDown())
            {
                this->perform( wxT("Sqrt") );
            }
            else
            {
                const char sz[] = {iKeyCode,0};
                this->perform( ISO2WX(sz) );
            }
        }
        else
            _e.Skip();
    }
    else
    {
        if (iKeyCode == WXK_BACK)
        {
            this->perform( wxT("<-") );
        }
        else if (iKeyCode == WXK_DELETE)
        {
            this->perform( wxT("Clr") );
        }
        else if (iKeyCode == WXK_RETURN || iKeyCode == WXK_NUMPAD_ENTER)
        {
            this->perform( wxT("=") );
        }
        else
            _e.Skip();
    }
}


/** une action a été réalisée sur le composant */
void TCalculator::perform(const wxString & sAction)
{
    if(!this->display)
        return;
    
    wxString s = this->display->getText();
    if(s == wxT("0"))
        s = wxT("");
    
    if(sAction == wxT("Clr"))
        s = wxT("0");
    else if(sAction == wxT("="))
    {
        try
        {
            double d = evaluate(s);
            if(d == 0.0)
                s = wxT("0");
            else
            {
                s = wxString::Format(wxT("%f"),d);
                // suppression des espaces de fin
                while( s.GetChar(s.Length()-1) == '0')
                    s.Truncate(s.Length()-1);
                s.Replace(wxT(","),wxT(".")); // gestion de la localisation
                if(s.GetChar(s.Length()-1) == '.')
                    s.Truncate(s.Length()-1);
            }
        }
        catch (InvalidExpressionException &e)
        {
            wxMessageBox(e.getMessage(), wxTr("Error"), wxOK | wxICON_ERROR, this);
        }
    }
    else if(sAction == wxT("<-"))
    {
        if(s)
            s = s.Left(s.Length()-1);
    }
    else if(sAction == wxT("Sqrt"))
        this->perform(wxT("Sqrt("), s);
    else if(sAction == ISO2WX(szCarre_TCALCULATOR))
        s += wxT("^2");
    else if(sAction == wxT("Cos"))
        this->perform(wxT("Cos("), s);
    else if(sAction == wxT("Sin"))
        this->perform(wxT("Sin("), s);
    else if(sAction == wxT("Tan"))
        this->perform(wxT("Tan("), s);
    else
        s += sAction;
    
    if(!s.Length())
        s = wxT("0");
    this->display->setText(s);
}

void TCalculator::perform(const wxString & sFunction, wxString & sFormula)
{
    if(sFormula.Length() == 0)
        sFormula = sFunction;
    else if(isdigit(sFormula.GetChar(sFormula.Length()-1)))
        sFormula = sFunction + sFormula;
    else
        sFormula += sFunction;
}

/** évalue une expression mathématique */
double TCalculator::evaluate(const wxString & sExp) throw (InvalidExpressionException)
{
    int iPos = 0;
    return readFormula(sExp, iPos);
}

double TCalculator::readFormula(const wxString & sExp, int & iStartPos) throw (InvalidExpressionException)
{
    if(sExp.Length() <= uint(iStartPos))
        throw InvalidExpressionException(wxTr("Incomplete formula 1"));
    
    int iPart = 0;
    double dTerm[4];
    char cOp[3];
    
    while(sExp.Length() > uint(iStartPos))
    {
        dTerm[iPart] = readTerminal(sExp, iStartPos);
        
        if(iPart > 0 && cOp[iPart - 1] == '^')
        {
            // reduction automatique de la puissance
            dTerm[iPart - 1] = pow(dTerm[iPart - 1],dTerm[iPart]);
            iPart--;
        }
        
        if(iPart > 1)
        {
            // on réduit ?
            if(cOp[iPart - 1] == '*')
            {
                double d = dTerm[iPart - 1] * dTerm[iPart];
                iPart--;
                dTerm[iPart] = d;
            }
            else if(cOp[iPart - 1] == '/')
            {
                double d = dTerm[iPart - 1] / dTerm[iPart];
                iPart--;
                dTerm[iPart] = d;
            }
            else if(cOp[iPart - 1] == '-')
            {
                double d = dTerm[iPart - 1] - dTerm[iPart];
                iPart--;
                dTerm[iPart] = d;
            }
            else if(cOp[iPart - 1] == '+')
            {
                double d = dTerm[iPart - 1] + dTerm[iPart];
                iPart--;
                dTerm[iPart] = d;
            }
        }
        
        if(iPart > 0)
        {
            // possibilité de réduire ?
            if(cOp[iPart - 1] == '*')
            {
                double d = dTerm[iPart - 1] * dTerm[iPart];
                iPart--;
                dTerm[iPart] = d;
            }
            else if(cOp[iPart - 1] == '/')
            {
                double d = dTerm[iPart - 1] / dTerm[iPart];
                iPart--;
                dTerm[iPart] = d;
            }
        }
        
        if(sExp.Length() > uint(iStartPos))
        {
            // il reste des choses a lire => un operateur entre 2 terminaux
            wxChar c = sExp.GetChar(iStartPos);
            if(c == '+' || c == '-' || c == '*' || c == '/' || c == '^')
            {
                iStartPos++;
                cOp[iPart] = c;
                
                if(iPart == 1 && (c == '+' || c == '-'))
                {
                    // 2 operateurs non-prioritaires a la suite : on reduit
                    if(cOp[0] == '+')
                    {
                        dTerm[0] = dTerm[0] + dTerm[1];
                        cOp[0] = c;
                        iPart--;
                    }
                    if(cOp[0] == '-')
                    {
                        dTerm[0] = dTerm[0] - dTerm[1];
                        cOp[0] = c;
                        iPart--;
                    }
                }
                iPart++;
            }
            else if(c == ')')
            {
                iStartPos++;
                break;
            }
            else
                throw InvalidExpressionException(wxString::Format(wxTr("Unknown operator : %c"),c));
        }
    }
    
    if(iPart > 0)
    {
        // on réduit
        if(cOp[iPart - 1] == '*')
        {
            double d = dTerm[iPart - 1] * dTerm[iPart];
            iPart--;
            dTerm[iPart] = d;
        }
        else if(cOp[iPart - 1] == '/')
        {
            double d = dTerm[iPart - 1] / dTerm[iPart];
            iPart--;
            dTerm[iPart] = d;
        }
        else if(cOp[iPart - 1] == '-')
        {
            double d = dTerm[iPart - 1] - dTerm[iPart];
            iPart--;
            dTerm[iPart] = d;
        }
        else if(cOp[iPart - 1] == '+')
        {
            double d = dTerm[iPart - 1] + dTerm[iPart];
            iPart--;
            dTerm[iPart] = d;
        }
    }
    // evaluer ce qu'on a lu
    return dTerm[0];
}

double TCalculator::readNumber(const wxString & sExp, int & iStartPos) throw (InvalidExpressionException)
{
    if(sExp.Length() <= uint(iStartPos))
        throw InvalidExpressionException(wxTr("Incomplete formula 2"));

    bool bNegative = false;
    wxChar c = sExp.GetChar(iStartPos);
    if(c == '-')
    {
        bNegative = true;
        iStartPos++;
    }

    if(!isdigit(sExp.GetChar(iStartPos)))
    {
        throw InvalidExpressionException(wxString::Format(wxTr("Invalid number format at %d"), iStartPos));
    }

    bool bDotRead = false;
    double dMult = 0.0;
    double dNum = 0.0;
    int iNbDigits = 0;
    while(sExp.Length() > uint(iStartPos))
    {
        c = sExp.GetChar(iStartPos);
        if(isdigit(c))
        {
            iNbDigits++;
            if(!bDotRead)
            {
                dNum *= 10.0;
                dNum += c - '0';
            }
            else
            {
                dNum += (c - '0') * dMult;
                dMult *= 0.1;
            }
        }
        else if(c == '.')
        {
            if(bDotRead)
                throw InvalidExpressionException(wxString::Format(wxTr("Invalid number format at %d"), iStartPos));

            bDotRead = true;
            
            dMult = 0.1;
        }
        else
            break;
        iStartPos++;
    }

    if(!iNbDigits)
        throw InvalidExpressionException(wxString::Format(wxTr("Invalid number format at %d"), iStartPos));
    
    return dNum;
}

double TCalculator::readTerminal(const wxString & sExp, int & iStartPos) throw (InvalidExpressionException)
{
    if(sExp.Length() <= uint(iStartPos))
        throw InvalidExpressionException(wxTr("Incomplete formula 3"));
    
    while(sExp.Length() > uint(iStartPos))
    {
        wxChar c = sExp.GetChar(iStartPos);
        
        // lecture du premier element
        if(isdigit(c)) // nombre
        {
            return readNumber(sExp,iStartPos);
        }
        else if(c == '-') // nombre negatif
        {
            return readNumber(sExp, iStartPos);
        }
        else if(c == '(') // une sous-formule
        {
            iStartPos++;
            return readFormula(sExp,iStartPos);
        }
        else if(c == 'S') // Sin ou Sqrt
        {
            if(sExp.Length() < uint(iStartPos + 6))
                throw InvalidExpressionException(wxTr("Incomplete formula 4"));
            
            wxChar c2 = sExp.GetChar(iStartPos+1);
            wxChar c3 = sExp.GetChar(iStartPos+2);
            wxChar c4 = sExp.GetChar(iStartPos+3);
            wxChar c5 = sExp.GetChar(iStartPos+4);
            if(c2 == 'i' && c3 == 'n' && c4 == '(')
            {
                iStartPos += 4;
                return sin(readFormula(sExp,iStartPos) / TCALCULATOR_RADIAN_DEGREE_RATIO);
            }
            else if(c2 == 'q' && c3 == 'r' && c4 == 't' && c5 == '(')
            {
                iStartPos += 5;
                return sqrt(readFormula(sExp,iStartPos));
            }
            else
            {
                throw InvalidExpressionException(wxString::Format(wxTr("Unexpected element at %d"), iStartPos));
            }
        }
        else if(c == 'C') // Cos
        {
            if(sExp.Length() < uint(iStartPos + 6))
                throw InvalidExpressionException(wxTr("Incomplete formula 5"));
            
            wxChar c2 = sExp.GetChar(iStartPos+1);
            wxChar c3 = sExp.GetChar(iStartPos+2);
            wxChar c4 = sExp.GetChar(iStartPos+3);
            if(c2 == 'o' && c3 == 's' && c4 == '(')
            {
                iStartPos += 4;
                return cos(readFormula(sExp,iStartPos) / TCALCULATOR_RADIAN_DEGREE_RATIO);
            }
            else
            {
                throw InvalidExpressionException(wxString::Format(wxTr("Unexpected element at %d"), iStartPos));
            }
        }
        else if(c == 'T') // Tan
        {
            if(sExp.Length() < uint(iStartPos + 6))
                throw InvalidExpressionException(wxTr("Incomplete formula 6"));
            
            wxChar c2 = sExp.GetChar(iStartPos+1);
            wxChar c3 = sExp.GetChar(iStartPos+2);
            wxChar c4 = sExp.GetChar(iStartPos+3);
            if(c2 == 'a' && c3 == 'n' && c4 == '(')
            {
                iStartPos += 4;
                return tan(readFormula(sExp,iStartPos) / TCALCULATOR_RADIAN_DEGREE_RATIO);
            }
            else
            {
                throw InvalidExpressionException(wxString::Format(wxTr("Unexpected element at %d"), iStartPos));
            }
        }
        else
        {
            throw InvalidExpressionException(wxString::Format(wxTr("Unexpected element at %d"), iStartPos));
        }
    }
    
    throw InvalidExpressionException(wxTr("Incorrect formula"));
}

