/*jslint browser: true, white: false */

function dbg(msg)
{
    if (console && console.log) {
        console.log(msg);
    }
}

function ASSERT(condicao, descricao)
{
    if (! condicao) {
        alert("Problema no aplicativo:\r\n\r\n" + 
              descricao +
              "\r\n\r\nPor favor avise o mantenedor do site.");
        console.trace();
        throw("Assert failed");
    }
    return condicao;
}

function AUDIT(condicao, descricao)
{
    if (! condicao) {
        alert("Erro de processamento:\r\n\r\n" + 
              descricao +
              "\r\n\r\nPor favor avise o mantenedor do site, se possível\r\n" +
              "mandando um exemplo dos lançamentos para teste.");
    }
    return condicao;
}

function tzoffset(d)
{
    // returns the time zone offset, expressed as "hours *behind* UTC".
    // that would be 180 minutes for Brazil (-0300) and -60 minutes for Germany (+0100)
    return d.getTimezoneOffset() * 60000;
}

function zeropad(s, n)
{
    s = "" + s;
    while (s.length < n) {
        s = "0" + s;
    }
    return s;
}

function dtoD(data)
{
    ASSERT(typeof data == "number", "parametro de dtoD");
    var a = new Date();
    a.setTime(data);
    return a;
}

function jDtod(data)
{
    return data.getTime();
}

function ymdtoD(y, m, d)
{
    // at noon, to prevent a daylight saving timezone to change the date.
    return new Date(y, m, d, 12, 0, 0);
}

function ymdtod(y, m, d)
{
    return jDtod(ymdtoD(y, m, d));
}

function agora()
{
    var a = new Date();
    return ymdtod(a.getFullYear(), a.getMonth(), a.getDate());
}

function ctod(s)
{
    if (! s) {
        return null;
    }

    var tks = s.split('/');
    if (tks.length != 3) {
        return null;
    }

    for(var i = 0; i < 3; ++i) {
        tks[i] = parseInt(tks[i], 10);
        if (isNaN(tks[i])) {
            return null;
        }
    }

    // pt-br
    var tmp = tks[0];
    tks[0] = tks[2];
    tks[2] = tmp;

    if (tks[0] < 100) {
        tks[0] += 2000;
    }

    if (tks[0] < 0 || tks[0] > 2099 || tks[1] < 1 || tks[1] > 12 || tks[2] < 1 || tks[2] > 31) {
        return null;
    }

    tks[1]--;

    var y = ctod.normal_year;
    if (((tks[0] % 4) === 0) && (((tks[0] % 100) !== 0) || ((tks[0] % 400) === 0))) {
        y = ctod.leap_year;
    }

    if (tks[2] > y[tks[1]]) {
        return null;
    }

    return ymdtod(tks[0], tks[1], tks[2]);
}

ctod.normal_year = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
ctod.leap_year = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

function dtoc(t)
{
    ASSERT(typeof t == "number", "parametro de dtoc " + typeof t);
    // t comes from cookie memory; since it came straight from getTime(),
    // setTime() will remove the timezone offset added by getTime(),
    // and we get the original date anyway.
    var exp = dtoD(t);
    // return ""+zeropad(exp.getFullYear().toFixed(0), 4)+"/"+zeropad((exp.getMonth()+1).toFixed(0), 2)+"/"+zeropad(exp.getDate().toFixed(0), 2);
    return ""+zeropad(exp.getDate().toFixed(0), 2)+"/"+zeropad((exp.getMonth()+1).toFixed(0), 2)+"/"+zeropad(exp.getFullYear().toFixed(0), 4);
}

function sgn(n)
{
    return (n > 0 ? 1 : (n < 0 ? -1 : 0));
}

function _mesmo_sinal(a, b)
{
    return sgn(a) == sgn(b);
}

function mesmo_sinal(a, b) {
    return (a === 0 || b === 0 || _mesmo_sinal(a, b));
}

function sinal_oposto(a, b) {
    return ! mesmo_sinal(a, b);
}

function a_procura(a, criteria, arg_adicional)
{
    for(var i = 0; i < a.length; ++i) {
        if (criteria(a[i], arg_adicional)) {
            return i;
        }
    }
    return -1;
}

function a_existe(a, criteria, arg_adicional)
{
    return (a_procura(a, criteria, arg_adicional) != -1);
}

function a_remove(a, i)
{
    a.splice(i, 1);
}

function htoc(hora)
{
    return zeropad(Math.floor(hora / 100).toFixed(0), 2) + ":" + 
           zeropad((hora % 100).toFixed(0), 2);

}

function ctoh(chora)
{
    var hora = parseInt(chora.replace(/[^0-9]/g, ''), 10);

    if (isNaN(hora)) {
        hora = 0;
    } else {
        hora = Math.round(hora);
        if (hora <= 0 || hora > 2359 || (hora % 100) > 59) {
            hora = 0;
        }
    }
    return hora;
}

function max_abs(a, b)
{
    return (Math.abs(a) > Math.abs(b)) ? a : b;
}

function min_abs(a, b)
{
    return (Math.abs(a) < Math.abs(b)) ? a : b;
}

function ultimo_dia_mes(data)
{
    AUDIT(typeof data == "number", "parametro de ultimo_dia_mes");
    var a = dtoD(data);
    return ymdtod(a.getFullYear(), a.getMonth() + 1, 0);
}

function ultimo_dia_proximo_mes(data)
{
    AUDIT(typeof data == "number", "parametro de ultimo_dia_proximo_mes");
    var a = dtoD(data);
    return ymdtod(a.getFullYear(), a.getMonth() + 2, 0);
}

function ultimo_dia_util_proximo_mes(data)
{
    AUDIT(typeof data == "number", "parametro de ultimo_dia_util_proximo_mes");
    var a = dtoD(data);
    a = ymdtoD(a.getFullYear(), a.getMonth() + 2, 0);

    while (a.getDay() < 1 || a.getDay() > 5) {
        a = ymdtoD(a.getFullYear(), a.getMonth(), a.getDate() - 1);
    }

    return jDtod(a);
}

/* retorna dia da semana normalizado com 0 = segunda-feira,
   em vez de 0 = domingo como o Javascript retornaria */

function data_dow(data)
{
    ASSERT(typeof data == "number", "parametro de data_dow");
    return (dtoD(data).getDay() + 6) % 7;
}

function data_ano(data)
{
    ASSERT(typeof data == "number", "parametro de data_ano");
    return dtoD(data).getFullYear();
}

function data_mes(data)
{
    ASSERT(typeof data == "number", "parametro de data_mes");
    return dtoD(data).getMonth();
}

function tipo_papel(papel)
{
    var caracteres = 0;
    var digitos = 0;
    var resto = 0;
    var valor = 0;
    var tipo = tipo_papel.DESCONHECIDO;
    var c, i;

    for (i = 0; i < papel.length; ++i) {
        c = papel.charAt(i);
        if (c >= 'A' && c <= 'Z') {
            ++caracteres;
        } else {
            break;
        }
    }

    for (i = caracteres; i < papel.length; ++i) {
        c = papel.charAt(i);
        if (c >= '0' && c <= '9') {
            ++digitos;
        } else {
            break;
        }
    }

    resto = papel.length - caracteres - digitos;

    if (digitos === 0) {
        return tipo;
    }

    valor = parseInt(papel.substr(caracteres, digitos), 10);

    if (caracteres === 5 && digitos == 2 && resto === 0 && papel.charAt(4) <= 'X') {
        tipo = tipo_papel.OPCAO;
    } else if (caracteres == 4 && digitos >= 1 && digitos <= 2 && resto === 0) {
        tipo = tipo_papel.ACAO;
    } else if (caracteres == 3 && digitos >= 1 && digitos <= 2 && resto === 0) {
        tipo = tipo_papel.FUTURO;
    }

    return tipo;
}

tipo_papel.DESCONHECIDO = 0;
tipo_papel.ACAO = 1;
tipo_papel.OPCAO = 5;
tipo_papel.FUTURO = 6;

/* Retorna a data do vencimento de uma opção (3a segunda do mês)
   mais 7 dias, depois do que é certo que a opção já não existe */

function vencimento_opcao(papel, data_compra)
{
    AUDIT(typeof data_compra == "number", "parametro data_compra de vencimento_opcao");
    AUDIT(papel.length > 5, "parametro papel de vencimento_opcao");
    var ano = data_ano(data_compra);
    var mes = (papel.charCodeAt(4) - 'A'.charCodeAt(0)) % 12;
    AUDIT((mes >= 0 && mes < 12), "vencimento_opcao: mes opcao");
    if (data_mes(data_compra) > mes) {
        ++ano;
    }
    // 22 é o menor dia que pode cair a 4a segunda do mês!
    var tentative = ymdtod(ano, mes, 22);
    var venc7 = ymdtod(ano, mes, 22 + ((7 - data_dow(tentative)) % 7));
    return venc7;
}

function formatFinancial(val, decs)
{
    if (typeof(decs) == "undefined") {
        decs = 2;
    }

    var cval = "" + Math.round(Math.abs(val));

    while (cval.length <= decs) {
        cval = "0" + cval;
    }

    var cents = cval.substr(cval.length - decs, decs);
    var intg = cval.substr(0, cval.length - decs);
    var sign = val < 0 ? "-" : "";
    var intgsep = "";
    
    while (intg.length > 0) {
        if (intg.length > 3) {
            var blk = intg.substr(intg.length - 3, 3);
            intg = intg.substr(0, intg.length - 3);
            intgsep = "." + blk + intgsep;
        } else {
            intgsep = intg + intgsep;
            intg = "";
        }
    }
    
    return sign + intgsep + (decs > 0 ? ("," + cents) : "");
}

function addMinutes(t, d)
{
    var m = t % 100;
    var h = (t - m) / 100;
    m += d;
    while (m >= 60) {
        ++h;
        m -= 60;
    }
    while (m < 0) {
        --h;
        m += 60;
    }
    while (h < 0) {
        h += 24;
    }
    h = h % 24;
    return h * 100 + m;
}
/*jslint browser: true, white: false */
/*global tela, main*/

ANIM_ITEM = 900;

function Lancto() {
    this.versao = Lancto._VERSAO_REGISTRO;
    this.ok = 0;
    this.htmlnode = null;
    this.err = "Lançamento não verificado";
}

Lancto._VERSAO_REGISTRO = 13;

var Lancto_class = Lancto.prototype;

Lancto_class.verificar = function (default_id)
{
    if ((typeof this.id != "number") || isNaN(this.id)) {
        this.id = default_id;
    }

    if (typeof this.qtde != "number") {
        this.qtde = this.qtde.replace(/[^\-0-9]/g, '');
        this.qtde = parseInt(this.qtde, 10);
    }

    if (isNaN(this.qtde)) {
        this.qtde = 0;
    }

    this.qtde = Math.round(this.qtde, 0);

    if (this.qtde === 0) {
        this.err = "Quantidade não pode ser igual a zero";
        return 0;
    }

    if (typeof this.corretagem != "number") {
        this.corretagem = this.corretagem.replace(/[^0-9]/g, '');
        this.corretagem = parseInt("0"+this.corretagem, 10);
    }

    if (isNaN(this.corretagem)) {
        this.corretagem = 0;
    }

    this.corretagem = Math.round(this.corretagem);

    if (this.corretagem < 0) {
        this.err = "Corretagem não pode ser menor que zero";
        return 0;
    }

    if (typeof this.vlunit != "number") {
        this.vlunit = this.vlunit.replace(/[^0-9]/g, '');
        this.vlunit = parseInt("0" + this.vlunit, 10);
    }

    if (isNaN(this.vlunit)) {
        this.vlunit = 0;
    }

    this.vlunit = Math.round(this.vlunit);

    if (this.vlunit < 0) {
        this.err = "Valor unitário não pode ser menor que zero";
        return 0;
    }

    if (typeof this.vltotal != "number") {
        this.vltotal = this.vltotal.replace(/[^0-9]/g, '');
        this.vltotal = parseInt("0" + this.vltotal, 10);
    }

    if (isNaN(this.vltotal)) {
        this.vltotal = 0;
    }

    this.vltotal = Math.round(this.vltotal);

    if (this.vltotal < 0) {
        this.err = "Valor total deve ser positivo (valor absoluto)";
        return 0;
    }

    if (this.vlunit > 0 && this.vltotal > 0) {
        var vteste = this.vlunit * this.qtde;
        this.valor_total = this.vltotal * sgn(this.qtde);
        if (vteste != this.valor_total) {
            this.err = "Valor total não bate com valor unitário; corrija ou preencha apenas um deles";
            return 0;
        }
        this.valor_unitario = this.vlunit;
    } else if (this.vlunit > 0) {
        this.valor_unitario = this.vlunit;
        this.valor_total = this.valor_unitario * this.qtde;
    } else {
        this.valor_total = this.vltotal * sgn(this.qtde);
        this.valor_unitario = this.valor_total / this.qtde;
    }

    this.papel = jQuery.trim(this.papel).toUpperCase();
    this.papel = this.papel.replace(/[^A-Z0-9]/g, '');

    if (this.papel.length < 1) {
        this.err = "Preencha o código do papel";
        return 0;
    }

    if (typeof this.data != "number") {
        this.data = ctod(this.data);
    }

    if (! this.data || isNaN(this.data)) {
        this.err = "Data inválida";
        return 0;
    }

    if (typeof this.hora != "number") {
        this.hora = ctoh(this.hora);
    }

    if (! this.hora || isNaN(this.hora)) {
        this.err = "Hora inválida";
        return 0;
    }

    this.ok = 1;
    this.err = null;

    return this.ok;
};

Lancto_class.remover_provisorios = function () {
    delete this.ok;
    delete this.err;
};

Lancto_class.copia_para_exportacao = function () {
    var copia = jQuery.extend({}, this);
    copia.remover_provisorios();
    delete copia.htmlnode;
    delete copia.valor_unitario;
    delete copia.valor_total;
    for (item in copia) {
        if (typeof copia[item] === "function" || typeof copia[item] === "object") {
            delete copia[item];
        }
    }
    return copia;
};

Lancto.criar = function (default_id, id, data, hora, papel, qtde, vlunit, vltotal, corretagem)
{
    l = new Lancto();

    l.id = id;
    l.data = data;
    l.hora = hora;
    l.papel = papel;
    l.qtde = qtde;
    l.vlunit = vlunit;
    l.vltotal = vltotal;
    l.corretagem = corretagem;

    l.verificar(default_id);

    return l;
};

Lancto.incluir_linha_vazia = function ()
{
    bgclasse = "zebra1";
    var nova_linha = $("<tr></tr>");
    nova_linha.attr('class', bgclasse);
    var nova_coluna = $("<td></td>");
    nova_coluna.attr("colspan", "10");
    nova_coluna_span = $("<span></span>");
    nova_coluna.append(nova_coluna_span);
    nova_linha.append(nova_coluna);
    tela.table_lanctos.append(nova_linha);
};

Lancto_class.incluir_na_tela = function(animar, bgclasse, prox_data)
{
    prox_data = -1;

    bgclasse = ["zebra0", "zebra1", "novo0", "novo1"][bgclasse];

    this.htmlnode = $("<tr style='display: none'></tr>");
    var ID = "rowlanc" + this.id;
    this.htmlnode.attr('id', ID);
    this.htmlnode.attr('class', bgclasse);

    var nova_coluna;
    var items = [];
    var classes = [];
    var td_classes = [];
    var classe = (this.qtde < 0 ? "dispendio" : "");

    items.push((this.data != prox_data) ? dtoc(this.data) : "");
    classes.push("");
    td_classes.push("numero");

    items.push(htoc(this.hora));
    classes.push("");
    td_classes.push("numero");

    items.push(this.papel);
    classes.push("");
    td_classes.push("");

    items.push(formatFinancial(this.qtde, 0));
    classes.push(classe);
    td_classes.push("numero");

    items.push("×");
    classes.push(classe);
    td_classes.push("numero");

    items.push(formatFinancial(this.valor_unitario));
    classes.push(classe);
    td_classes.push("numero");

    items.push("=");
    classes.push(classe);
    td_classes.push("numero");

    items.push(formatFinancial(this.valor_total));
    classes.push(classe);
    td_classes.push("numero");

    items.push(this.corretagem !== 0 ? "+" + formatFinancial(this.corretagem) : "");
    classes.push("");
    td_classes.push("numero");

    var item;
    for (var i = 0; i < items.length; ++i) {
        item = items[i];
        nova_coluna = $("<td></td>");
        nova_coluna.addClass(td_classes[i]);
        nova_coluna_span = $("<span></span>");
        nova_coluna_span.html(item);
        nova_coluna_span.addClass(classes[i]);
        nova_coluna.append(nova_coluna_span);
        this.htmlnode.append(nova_coluna);
    }

    var link_remover = $("<a href='#'></a>");
    // link_remover.attr("href", "javascript:main.remover(" + this.id + ")");
    var closure_id = this.id;
    link_remover.click(function () { main.remover(closure_id); return false; });
    link_remover.html("remover");
    // nova_coluna_span = document.createElement("span");
    // nova_coluna_span.append(link_remover);
    nova_coluna = $("<td></td>");
    // nova_coluna.append(nova_coluna_span);
    nova_coluna.append(link_remover);
    this.htmlnode.append(nova_coluna);

    tela.table_lanctos.prepend(this.htmlnode);
    
    this.htmlnode.fadeIn(ANIM_ITEM);
};

Lancto_class.remover_da_tela = function(animar) {
    if (this.htmlnode) {
        this.deleted = true;
        // closure to avoid the changing meaning of 'this'
        var clos = this;
        clos.htmlnode.fadeOut(animar ? ANIM_ITEM : 0, function() {
            clos.htmlnode.remove();
            clos.htmlnode = null;
        });
    }
};

/*jslint browser: true, white: false */
/*global exibir, main, tela*/

function Main() {
    this.lanctos = [];
    this.last_id = 1;
    this.bgnovo = 0;
    this.arquivo_salvo = "taxman.txt";

    this.lucro_normal = 0;
    this.lucro_daytrade = 0;
    this.imposto = 0;

    this.storage = null;
}

Main.DIARIO = 1;
Main.MENSAL = 2;
Main.ANUAL = 4;
Main.TUDO  = 8;
Main.POR_PAPEL = 16;

var Main_class = Main.prototype;

Main_class.vazio = function ()
{
    return this.lanctos.length <= 0;
};

Main_class.ler_storage = function (engine)
{
    if (this.storage) {
        // Bug esquisitíssimo do IE7
        return;
    }
    this.storage = engine;

    var lanctos_raw = this.storage.get("lanctos");
    var arquivo_raw = this.storage.get("lanctos_arquivo");

    if (! lanctos_raw) {
        lanctos_raw = [];
    }
    if (! arquivo_raw) {
        arquivo_raw = [ this.arquivo_salvo ];
    }
    if (! arquivo_raw[0]) {
        arquivo_raw = [ this.arquivo_salvo ];
    }
    if (arquivo_raw[0] == "undefined") {
        arquivo_raw = [ this.arquivo_salvo ];
    }
    
    this.arquivo_salvo = arquivo_raw[0];
    this.ler_storage2(lanctos_raw);
};

Main_class.ler_storage2 = function (lanctos_raw) {
    var lancto, l;
    var teste_existe = function (arg1, arg2) { return arg1.id == arg2.id; };
    this._remover_tudo();

    for (var i = 0; i < lanctos_raw.length; ++i) {
        l = lanctos_raw[i];

        if (a_existe(this.lanctos, teste_existe, l)) {
            continue;
        }

        if (! l.versao) {
            continue;
        }

        if (l.versao != Lancto._VERSAO_REGISTRO) {
            if (l.versao < 13) {
                // upgrade para 13
                l.vltotal = 0;
            } else {
                continue;
            }
        }

        if (! l.id) {
            continue;
        }

        if (! l.papel) {
            continue;
        }

        if (! l.qtde) {
            continue;
        }

        if (l.vlunit === undefined) {
            continue;
        }

        if (l.vltotal === undefined) {
            continue;
        }

        if (l.corretagem === undefined) {
            continue;
        }

        if (! l.data) {
            continue;
        }

        if (! l.hora) {
            continue;
        }

        lancto = Lancto.criar(this.last_id, l.id, l.data, l.hora, l.papel, 
                              l.qtde, l.vlunit, l.vltotal, l.corretagem);
        if (! lancto.ok) {
            continue;
        }
        lancto.remover_provisorios();

        this.lanctos.push(lancto);
        if (lancto.id > this.last_id) {
            this.last_id = lancto.id + 1;
        }
    }

    this.consolidar();
    this.mostrar_tudo_na_tela();
    exibir("lanctos", true);
};


Main_class.mostrar_tudo_na_tela = function () {
    var zebra = this.lanctos.length % 2;
    var prox_data = -1;
    var lancto, lancto2;

    // this.lanctos.reverse();
    for (var j = 0; j < this.lanctos.length; ++j) {
        lancto = this.lanctos[j];
        lancto2 = this.lanctos[parseInt(j, 10)+1];
        if (lancto2) {
            prox_data = lancto2.data;
        } else {
            prox_data = -1;
        }
        lancto.incluir_na_tela(0, zebra, prox_data);
        zebra = zebra ? 0 : 1;
    }
    // this.lanctos.reverse();

    if ((this.lanctos.length % 2) === 0) {
        Lancto.incluir_linha_vazia();
    }
};

Main_class.gravar_storage = function ()
{
    var lista_exp = [];

    for (var k = 0; k < this.lanctos.length; ++k) {
        lista_exp.push(this.lanctos[k].copia_para_exportacao());
   }

   try {
       this.storage.set("lanctos", $.toJSON(lista_exp));
       this.storage.set("lanctos_arquivo", $.toJSON([ this.arquivo_salvo ]));
   } catch (exp) {
       alert("Armazenamento local do lançamento falhou. Provavelmente seu navegador não é compativel com este recurso. Tente usar outro navegador, como o Firefox.");
    }
};

Main_class.ordenar = function() 
{
    this.lanctos.sort( function (a, b) {
        return (a.data * 10000 + a.hora) - (b.data * 10000 + b.hora);
    });
};

Main_class.is_lancto_novo = function (lancto)
{
    var i = a_procura(this.lanctos, function(a) { return a.data == lancto.data && a.hora == lancto.hora; });
    return i <= -1;
};

Main_class.erro = function(serr)
{
    tela.err.html(serr);
};

Main_class.adicionar = function () 
{
    this.erro("");

    var data = $("#form_data").val();
    var hora = $("#form_hora").val();
    var papel = $("#form_papel").val();
    var qtde = $("#form_qtde").val();
    var vlunit = $("#form_vlunit").val();
    var vltotal = $("#form_vltotal").val();
    var corretagem = $("#form_corretagem").val();

    hora = ctoh(hora);
    var hora_salva = hora;

    var lancto = Lancto.criar(null, ++this.last_id, data, hora, papel, qtde, vlunit, vltotal, corretagem);

    if (lancto.ok) {
        lancto.remover_provisorios();
        if (! this.is_lancto_novo(lancto)) {
            this.erro("Já existe lançamento com esta data/hora");
        } else {
            this.lanctos.push(lancto);
            lancto.incluir_na_tela(1, 2 + this.bgnovo, -1);
            this.consolidar();
            if (this.lanctos.length == 1) { // portanto estava vazia até há pouco
                exibir("lanctos", false);
            }
            this.bgnovo = (this.bgnovo ? 0 : 1);
            
            hora_salva = addMinutes(hora_salva, 15);
            $("#form_hora").val(htoc(hora_salva));
        }
    } else {
        this.erro(lancto.err);
    }
};

Main_class._remover = function(i, animar) 
{
    if (this.lanctos[i]) {
        this.lanctos[i].remover_da_tela(animar);
        a_remove(this.lanctos, i);
    }
};

Main_class._remover_tudo = function() 
{
    while (! this.vazio()) {
        this._remover(0, false);
    }
};

Main_class.remover_tudo = function ()
{
    if (prompt("Digite SIM se você realmente deseja remover todos os lançamentos armazenados nesta página", "Não") == "SIM") {
        this._remover_tudo();
        this.consolidar();
    } else {
        alert("Remoção cancelada.");
    }
    // lista ficou vazia, esconder tudo
    exibir("lanctos", false);
};

Main_class.remover = function (id)
{
    var i = a_procura(this.lanctos, function(a) { return a.id == id; });
    if (i > -1) {
        var lancto = this.lanctos[i];
        if (confirm("Confirma a remoção do lançamento " + formatFinancial(lancto.qtde, 0) + 
                " x " + lancto.papel + " do dia " + dtoc(lancto.data) + "?")) {
            this._remover(i, true);
            this.consolidar();
        }
    }
    if (this.vazio()) {
        // lista ficou vazia, esconder tudo
        exibir("lanctos", false);
    }
};

Main_class.consolidar = function ()
{
    this.gravar_storage();
    this.ordenar();
    this.processar();
    this.atualizar_saldos();
};

/*jslint browser: true, white: false */
 

Main_class.adicionar_saldo_tela = function (papel, qtde, valor, bgclasse)
{
    bgclasse = ["zebra0", "zebra1", "total"][bgclasse];

    var nova_linha = $("<tr></tr>");
    nova_linha.addClass(bgclasse);

    var nova_coluna;
    var items = [];
    var classes = [];
    var td_classes = [];
    var classe = (qtde < 0? "negativo" : "");
    

    if (papel) {
        items.push(papel);
        classes.push(classe);
        td_classes.push("");
    
        items.push(formatFinancial(qtde, 0));
        classes.push(classe);
        td_classes.push("numero");
    
        items.push(bgclasse == "total" ? "" : "×");
        classes.push(classe);
        td_classes.push("numero");
    
        items.push(bgclasse == "total" ? "" : formatFinancial(valor / qtde));
        classes.push(classe);
        td_classes.push("numero");
    
        items.push(bgclasse == "total" ? "" : "=");
        classes.push(classe);
        td_classes.push("numero");
    
        items.push(formatFinancial(valor));
        classes.push(classe);
        td_classes.push("numero");
    } else {
        for (var i = 1; i <= 6; ++i) {
            items.push("");
            classes.push("");
            td_classes.push("");
        }
    }
    
    var item;
    for (var j = 0; j < items.length; ++j) {
        item = items[j];
        nova_coluna = $("<td></td>");
        nova_coluna.addClass(td_classes[j]);
        nova_coluna_span = $("<span></span>");
        nova_coluna_span.addClass(classes[j]);
        nova_coluna_span.html(item);
        nova_coluna.append(nova_coluna_span);
        nova_linha.append(nova_coluna);
    }

    tela.table_saldos.append(nova_linha);
};


Main_class.adicionar_saldos_tela = function ()
{
    tela.table_saldos.children().each(function() {
        var t = $(this);
        t.remove();
    });

    var papeis = [];
    var total_qtde = 0, total_valor = 0;

    for (var k in this.saldos_qtde) {
        if (this.saldos_qtde[k] !== 0) {
            papeis.push(k);
        }
    }

    papeis.sort();
    var zebra = 1;

    for (var j = 0; j < papeis.length; ++j) {
        this.adicionar_saldo_tela(papeis[j], this.saldos_qtde[papeis[j]], this.saldos_valor[papeis[j]], zebra);
        total_qtde += this.saldos_qtde[papeis[j]];
        total_valor += this.saldos_valor[papeis[j]];
        zebra = zebra ? 0 : 1;
    }

    this.adicionar_saldo_tela("Total", total_qtde, total_valor, 2);

    /*
    if (zebra || papeis.length <= 0) {
        this.adicionar_saldo_tela(null, 1, 0, 1);
    }
    */
};


Main_class.atualizar_saldos = function ()
{
    this.adicionar_saldos_tela();

    valores = [this.lucro_normal, this.lucro_daytrade, this.lucro_normal + this.lucro_daytrade, this.imposto];
    for (var k = 0; k < valores.length; ++k) {
        tela.saldos[k].html(formatFinancial(valores[k]));
        tela.saldos[k].attr('class', (valores[k] < 0 ? "negativo" : ""));
    }
};

/*jslint browser: true, white: false */

var rotinas = [];

function Ticket(gerador1, gerador2, data, hora, papel, valor, nivel, texto, cargo)
{
    this.gerador1 = gerador1;
    this.gerador2 = gerador2;
    this.id = ++main.last_ticket_id;
    this.ticket = 1;
    this.data = data;
    this.hora = hora;
    this.ordem = 5;
    this.ordem_offset = 1;
    this.ddata = dtoD(data);
    this.papel = papel;
    this.qtde = 1; /* dummy */
    this.valor = valor;
    this.nivel = nivel;
    this.texto = texto;
    this.cargo = cargo;
    this.saldo_qtde = 0;
    this.saldo_valor = 0;
    this.daytrade = 0;
    this.enfeite = nivel >= 4 ? '\u2716 ' : (nivel >= 3 ? '\u2702  ' : '\u2714  ');
    this.classe = ["nivel1", "nivel2", "nivel3", "nivel4", "nivel5"][nivel];
}

Main_class.ordenar_concentrado = function() 
{
    this.concentrado.sort( function (a, b) {
        if (a.data == b.data) {
            if (a.hora == b.hora) {
                if (a.ticket == b.ticket) {
                    if (a.ticket) {
                        // ordem de criação dos tickets, no mesmo dia
                        return a.id - b.id;
                    } else {
                        return a.ordem - b.ordem;
                    }
                } else {
                    // tickets aparecem depois do lançamento
                    return a.ticket - b.ticket;
                }
            } else {
                return a.hora - b.hora;
            }
        } else {
            return a.data - b.data;
        }
    });
};

Main_class.adicionar_proc_tela = function (plancto, bgclasse, data_ant)
{
    data_ant = -1;
    bgclasse = ["zebra0", "zebra1"][bgclasse];

    var nova_linha = $("<tr></tr>");
    var nova_coluna, nova_coluna_span;

    if (! plancto) {
        nova_linha.attr('class', bgclasse);
        nova_coluna = $("<td></td>");
        nova_coluna.attr("colspan", "7");
        nova_linha.append(nova_coluna);
        nova_coluna = $("<td></td>");
        nova_coluna_span = $("<span></span>");
        nova_coluna.append(nova_coluna_span);
        nova_linha.append(nova_coluna);
    } else {
        nova_linha.attr('class', plancto.ticket ? plancto.classe : bgclasse);

        var items = [];
        var classes = [];
        var td_classes = [];
        var classe = plancto.ticket ? 
                (plancto.valor < 0 ? "dispendio" : "") :
                (plancto.qtde < 0 ? "dispendio" : "");

        items.push(plancto.data == data_ant ? "" : dtoc(plancto.data));
        classes.push("");
        td_classes.push("numero");
    
        items.push(plancto.ticket ? "" : htoc(plancto.hora));
        classes.push("");
        td_classes.push("numero");
    
        items.push(plancto.papel);
        classes.push("");
        td_classes.push("");
    
        items.push(plancto.ticket ? "" : formatFinancial(plancto.qtde, 0));
        classes.push(classe);
        td_classes.push("numero");
    
        if (plancto.ticket) {
            items.push(plancto.valor ? formatFinancial(plancto.valor) : "");
        } else {
            items.push(formatFinancial((plancto.valor_total + plancto.corretagem)));
        }
        classes.push(classe);
        td_classes.push("numero");

        var saldo_sufixo = plancto.daytrade ? " dt" : "";

        if (plancto.ticket) {
            items.push("");
        } else {
            items.push(formatFinancial(plancto.saldo_qtde, 0) + saldo_sufixo);
        }
        classes.push(plancto.saldo_qtde < 0 ? "dispendio" : "");
        td_classes.push("numero");

        if (plancto.ticket) {
            items.push("");
        } else {
            items.push(formatFinancial(plancto.saldo_valor) + saldo_sufixo);
        }
        classes.push(plancto.saldo_valor < 0 ? "dispendio" : "");
        td_classes.push("numero");

        items.push(plancto.ticket ? plancto.enfeite + plancto.texto : "");
        classes.push("");
        td_classes.push("");
    
        var item;
        for (var i = 0; i < items.length; ++i) {
            item = items[i];
            nova_coluna = $("<td></td>");
            nova_coluna.addClass(td_classes[i]);
            nova_coluna_span = $("<span></span>");
            nova_coluna_span.addClass(classes[i]);
            nova_coluna_span.html(item);
            nova_coluna.append(nova_coluna_span);
            nova_linha.append(nova_coluna);
        }
    }

    tela.table_proc.append(nova_linha);
};


Main_class.adicionar_procs_tela = function ()
{
    var zebra = 1;
    var data_ant = -1;
    var c;

    tela.table_proc.children().each(function() {
        var t = $(this);
        t.remove();
    });

    this.concentrado.reverse();
    for (var k = 0; k < this.concentrado.length; ++k) {
        c = this.concentrado[k];
        this.adicionar_proc_tela(c, zebra, data_ant);
        if (c.ticket) {
            zebra = 0;
        } else {
            zebra = zebra ? 0 : 1;
        }
        data_ant = c.data;
    }
    if (zebra) {
        this.adicionar_proc_tela(null, zebra);
    }
    this.concentrado.reverse();
};

Main_class.calcula_concentrado_total = function()
{
    this.concentrado = [];
    this.saldos_qtde = [];
    this.saldos_valor = [];

    for(var i = 0; i < this.lanctos.length; ++i) {
        var lancto = this.lanctos[i];
        var papel = lancto.papel;

        if (! this.saldos_qtde[papel]) {
            this.saldos_qtde[papel] = 0;
            this.saldos_valor[papel] = 0;
        }

        lancto_analise = jQuery.extend({}, lancto);
        lancto_analise.ticket = 0;

        lancto_analise.saldo_qtde = 0;
        lancto_analise.saldo_valor = 0;
        lancto_analise.daytrade = 0;

        lancto_analise.ddata = dtoD(lancto_analise.data);
        // serve para estabelecer ordem relativa entre versoes desmembradas do mesmo lancamento
        lancto_analise.ordem = 5;
        lancto_analise.ordem_offset = 1;

        this.concentrado.push(lancto_analise);
    }
};

Main_class.calcula_concentrados_parciais = function ()
{
    this.concentrado_papel = [];
    this.concentrado_papel_diario = [];
    this.concentrado_papel_mensal = [];
    this.concentrado_papel_anual = [];
    this.concentrado_diario = [];
    this.concentrado_mensal = [];
    this.concentrado_anual = [];

    this.ordenar_concentrado();

    for (var k = 0; k < this.concentrado.length; ++k) {
        var lancto = this.concentrado[k];
        var papel = lancto.papel;
        var ano = lancto.ddata.getFullYear();
        var mes = ano * 100 + lancto.ddata.getMonth();
        var dia = lancto.data;
        if (! this.concentrado_papel[papel]) {
            this.concentrado_papel[papel] = [];
            this.concentrado_papel_diario[papel] = [];
            this.concentrado_papel_mensal[papel] = [];
            this.concentrado_papel_anual[papel] = [];
        }

        if (! this.concentrado_papel_diario[papel][dia]) {
            this.concentrado_papel_diario[papel][dia] = [];
        }

        if (! this.concentrado_papel_mensal[papel][mes]) {
            this.concentrado_papel_mensal[papel][mes] = [];
        }

        if (! this.concentrado_papel_anual[papel][ano]) {
            this.concentrado_papel_anual[papel][ano] = [];
        }

        if (! this.concentrado_diario[dia]) {
            this.concentrado_diario[dia] = [];
        }

        if (! this.concentrado_mensal[mes]) {
            this.concentrado_mensal[mes] = [];
        }

        if (! this.concentrado_anual[ano]) {
            this.concentrado_anual[ano] = [];
        }

        this.concentrado_diario[dia].push(lancto);
        this.concentrado_mensal[mes].push(lancto);
        this.concentrado_anual[ano].push(lancto);
        this.concentrado_papel_diario[papel][dia].push(lancto);
        this.concentrado_papel_mensal[papel][mes].push(lancto);
        this.concentrado_papel_anual[papel][ano].push(lancto);
        this.concentrado_papel[papel].push(lancto);
    }
};

Main_class.processa1 = function(concentrado, rotina)
{
    var res = {tickets: [], desmembramentos: []};
    if (concentrado.length > 0) {
        // garante que rotina é chamada apenas quando há o que processar
        res = rotina(concentrado);
    }
    return res;
};

Main_class.processa2 = function(concentrado, rotina)
{
    var res = {tickets: [], desmembramentos: []};
    for (var k in concentrado) {
        if (true) {
            var cres = this.processa1(concentrado[k], rotina);
            res.tickets = res.tickets.concat(cres.tickets);
            res.desmembramentos = res.desmembramentos.concat(cres.desmembramentos);
        }
    }
    return res;
};

Main_class.processa3 = function(concentrado, rotina)
{
    var res = {tickets: [], desmembramentos: []};
    for (var k in concentrado) {
        if (true) {
            var cres = this.processa2(concentrado[k], rotina);
            res.tickets = res.tickets.concat(cres.tickets);
            res.desmembramentos = res.desmembramentos.concat(cres.desmembramentos);
        }
    }
    return res;
};

Main_class.processar = function ()
{
    var res;
    this.last_ticket_id = 0;
    var recalcular = 1;
    this.lucro_normal = this.lucro_daytrade = this.imposto = 0;

    this.calcula_concentrado_total();

    for (var k = 0; k < rotinas.length; ++k) {
        var rotina = rotinas[k];

        if (recalcular) {
            this.calcula_concentrados_parciais();
            recalcular = 0;
        }

        if (rotina.tipo & Main.POR_PAPEL) {
            if (rotina.tipo & Main.DIARIO) {
                res = this.processa3(this.concentrado_papel_diario, rotina);
            } else if (rotina.tipo & Main.MENSAL) {
                res = this.processa3(this.concentrado_papel_mensal, rotina);
            } else if (rotina.tipo & Main.ANUAL) {
                res = this.processa3(this.concentrado_papel_anual, rotina);
            } else if (rotina.tipo & Main.TUDO) {
                res = this.processa2(this.concentrado_papel, rotina);
            }
        } else {
            if (rotina.tipo & Main.DIARIO) {
                res = this.processa2(this.concentrado_diario, rotina);
            } else if (rotina.tipo & Main.MENSAL) {
                res = this.processa2(this.concentrado_mensal, rotina);
            } else if (rotina.tipo & Main.ANUAL) {
                res = this.processa2(this.concentrado_anual, rotina);
            } else if (rotina.tipo & Main.TUDO) {
                res = this.processa1(this.concentrado, rotina);
            }
        }

        if (res.desmembramentos.length > 0) {
            this.processar_desmembramentos(res.desmembramentos);
            recalcular = 1;
        }
        if (res.tickets.length > 0) {
            this.concentrado = this.concentrado.concat(res.tickets);
            recalcular = 1;
        }
    }

    this.ordenar_concentrado();
    this.adicionar_procs_tela();
};

Main_class.processar_desmembramentos = function(desmembramentos)
{
    var novos_lanctos = [];
    for(var k = 0; k < desmembramentos.length; ++k) {
        var D = desmembramentos[k];

        AUDIT(D.qtdes[0], "Desmembramento: lançamento original não pode ser zerado");
        AUDIT(D.original.qtde === (D.qtdes[0] + D.qtdes[1] + D.qtdes[2]), "Desmembramento: soma das partes deve ser igual ao original");

        var lancto0 = D.original;
        var totorig = D.original.valor_total;
        var vlu = 0;
        if (Math.abs(D.original.qtde) !== 0) {
           vlu = D.original.valor_total / D.original.qtde;
        }
        lancto0.ordem_offset /= 2;
        var lancto1 = jQuery.extend({}, lancto0);
        var lancto2 = jQuery.extend({}, lancto0);

        lancto0.qtde = D.qtdes[0];
        lancto1.qtde = D.qtdes[1];
        lancto2.qtde = D.qtdes[2];

        lancto0.daytrade = D.dt[0];
        lancto1.daytrade = D.dt[1];
        lancto2.daytrade = D.dt[2];

        lancto0.valor_total = Math.round(vlu * lancto0.qtde);
        lancto1.valor_total = Math.round(vlu * lancto1.qtde);
        lancto2.valor_total = totorig - lancto0.valor_total - lancto1.valor_total;

        lancto0.ordem = lancto0.ordem - lancto0.ordem_offset;
        // lancto1 herda a ordem do lançamento original
        lancto2.ordem = lancto2.ordem + lancto2.ordem_offset;

        if (lancto1.qtde) {
            this.concentrado.push(lancto1);
        } 

        if (lancto2.qtde) {
            this.concentrado.push(lancto2);
        }
    }
};


function rotina(tipo, funcao) {
    var closure = function (concentrado) {
        AUDIT(concentrado.length > 0, "Rotina deve receber concentrado não-vazio");
        var t = [], d = [];
        funcao(concentrado, t, d);
        return {tickets: t, desmembramentos: d};
    };
    closure.tipo = tipo;
    rotinas.push(closure);
}

function percorre_lancamentos(concentrado, funcao)
{
    for(var k = 0; k < concentrado.length; ++k) {
        var lancto = concentrado[k];
        if (lancto.ticket === 0) {
            if (funcao(lancto)) {
                break;
            }
        }
    }
}

function percorre_tickets(concentrado, funcao)
{
    for(var k = 0; k < concentrado.length; ++k) {
        var lancto = concentrado[k];
        if (lancto.ticket === 1) {
            if (funcao(lancto)) {
                break;
            }
        }
    }
}

function percorre_tudo(concentrado, funcao)
{
    for(var k = 0; k < concentrado.length; ++k) {
        var lancto = concentrado[k];
        if (funcao(lancto)) {
            break;
        }
    }
}

/*jslint browser: true, white: false */

/* Rotinas de processamento */

// desmembra lançamentos que invertem o saldo total

rotina(Main.TUDO | Main.POR_PAPEL, function (concentrado, tickets, desmembramentos)
{
    var saldo_ant = 0;
    percorre_lancamentos(concentrado, function (L) {
        L.daytrade = 0; // indefinido
        if (sinal_oposto(L.qtde, saldo_ant) && Math.abs(L.qtde) > Math.abs(saldo_ant)) {
            // sinal diferente e maior que saldo anterior = inversão de sinal
            desmembramentos.push({original: L, 
                          qtdes: [-saldo_ant, L.qtde + saldo_ant, 0],
                          dt: [0, 0, 0]});
        }
        saldo_ant += L.qtde;
    });
});

// calcula saldo atual de cada lançamento, para fins de detecção de daytrade

rotina(Main.TUDO | Main.POR_PAPEL, function (concentrado, tickets, desmembramentos)
{
    var saldo = 0;
    percorre_lancamentos(concentrado, function (L) {
        L._sq_ant = saldo;
        saldo += L.qtde;
    });
});


// detecta lançamentos DT e desmembra lançamentos zerocross DT

rotina(Main.DIARIO | Main.POR_PAPEL, function (concentrado, tickets, desmembramentos)
{
    var posicao = null;
    var saldo_dt, saldo_geral;

    percorre_lancamentos(concentrado, function (L) {
        if (posicao === null) {
            saldo_geral = posicao = L._sq_ant;
            saldo_dt = 0;
            delete L._sq_ant;
        }

        saldo_geral += L.qtde;

        if (saldo_dt !== 0 && sinal_oposto(saldo_dt, L.qtde)) {
            // saldo DT + um lançamento de sinal contrário = realização DT
        
            if (Math.abs(L.qtde) > Math.abs(saldo_dt)) {
                // liquida & inverte saldo DT
        
                var liquida_dt = -saldo_dt;
                var liquida_pos = 0;
                var reconstroi_dt = L.qtde + saldo_dt;

                if (sinal_oposto(reconstroi_dt, posicao)) {
                    // reconstroi_dt pode ser usado para queimar posição
                    var transf = min_abs(reconstroi_dt, -posicao);
                    liquida_pos += transf;
                    reconstroi_dt -= transf;
                }

                desmembramentos.push({original: L, 
                              qtdes: [liquida_dt, liquida_pos, reconstroi_dt], 
                              dt: [1, -1, 1]});

                saldo_dt += liquida_dt;
                posicao += liquida_pos;
                saldo_dt += reconstroi_dt;

            } else {
                // apenas liquida parte do saldo DT
                saldo_dt += L.qtde;
                L.daytrade = 1;
            }

        } else if (posicao !== 0 && sinal_oposto(posicao, L.qtde)) {
            // liquida posição
            if (Math.abs(L.qtde) > Math.abs(posicao)) {
                // liquida posição e ainda cria novo saldo DT
                desmembramentos.push({original: L, 
                              qtdes: [-posicao, L.qtde + posicao, 0],
                              dt: [-1, 1, 0]});
                saldo_dt = L.qtde + posicao;
                posicao = 0;
            } else {
                // liquida apenas parte da posição
                posicao += L.qtde;
                L.daytrade = -1;
            }

        } else {
            // cria saldo DT
            saldo_dt += L.qtde;
            L.daytrade = 1;
        }

        // como só vai aproveitar o último de cada dia, não conflita com desmembramentos
        L._sqdt = saldo_dt;
    });

    AUDIT(saldo_dt + posicao === saldo_geral, "Saldo geral diferente da soma de posição + daytrade");
});


// desmembra lançamentos que seriam DT mas deixaram saldo para o dia seguinte

rotina(Main.DIARIO | Main.POR_PAPEL, function (concentrado, tickets, desmembramentos)
{
    var posicao = null;

    // percorreremos a lista ao contrário
    concentrado.reverse();

    percorre_lancamentos(concentrado, function (L) {
        if (posicao === null) {
            posicao = L._sqdt;
            delete L._sqdt;
        }

        if (posicao !== 0) {
            if (mesmo_sinal(posicao, L.qtde)) {
                // este lançamento ajuda a criar posição não-DT
                if (Math.abs(L.qtde) > Math.abs(posicao)) {
                    // parte final do lançamento é posição: desmembrar
                    L.daytrade = 0; // assegura que desmembramento vai setar
                    desmembramentos.push({original: L,
                                  qtdes: [L.qtde - posicao, posicao, 0],
                                  dt: [1, -1, 0]});
                    posicao = 0;
                } else {
                    // este lancamento cria parte da posição
                    L.daytrade = -1;
                    // sinal negativo pois estamos percorrendo ao contrário
                    posicao -= L.qtde;
                }
            }
        } else {
            // nada mais a fazer
            return 1;
        }
        return null;
    });

    // desfaz inversão para outros poderem reaproveitar o concentrado
    concentrado.reverse();
});


// normaliza variável daytrade

rotina(Main.TUDO, function (concentrado, tickets, desmembramentos)
{
    percorre_lancamentos(concentrado, function (L) {
        AUDIT(L.daytrade !== 0, "Lançamento com status daytrade ambíguo: " + 
                        dtoc(L.data) + " " + htoc(L.hora));
        L.daytrade = L.daytrade > 0;
    });
});

// verifica que nenhum zero-cross ocorre no histórico do papel
// (sinal que os desmembramentos foram exatos)

rotina(Main.DIARIO | Main.POR_PAPEL, function (concentrado, tickets, desmembramentos)
{
    var saldo = 0, saldo_ant = 0;
    percorre_lancamentos(concentrado, function (L) {
        saldo += L.qtde;
        AUDIT(mesmo_sinal(saldo_ant, saldo), "Lançamento de posição cruza saldo zero no papel " + L.papel);
    });
});

// verifica que o saldo daytrade de cada dia é igual a zero
// e que nenhum zero-cross ocorre dentro do dia
// (sinal que os desmembramentos foram exatos)

rotina(Main.DIARIO | Main.POR_PAPEL, function (concentrado, tickets, desmembramentos)
{
    var saldo = 0, saldo_ant = 0, papel = "", data = ctod("01/01/1970");
    percorre_lancamentos(concentrado, function (L) {
        if (L.daytrade) {
            saldo += L.qtde;
            AUDIT(mesmo_sinal(saldo_ant, saldo), "Lançamento de daytrade cruza saldo zero no papel " + L.papel);
            saldo_ant = saldo;
            papel = L.papel;
            data = L.data;
        }
    });

    AUDIT(saldo === 0, "Saldo daytrade não zerado ao final do dia: " + papel + " " + dtoc(data));
});

// verifica que nenhum zero-cross ocorre para lançamentos normais
// (sinal que os desmembramentos foram exatos)

rotina(Main.DIARIO | Main.POR_PAPEL, function (concentrado, tickets, desmembramentos)
{
    var saldo = 0, saldo_ant = 0;
    percorre_lancamentos(concentrado, function (L) {
        if (! L.daytrade) {
            saldo += L.qtde;
            AUDIT(mesmo_sinal(saldo_ant, saldo), "Lançamento de posição cruza saldo zero no papel " + L.papel);
            saldo_ant = saldo;
        }
    });
});


// calcula saldos de valor e realizações normais (não DT)

rotina(Main.TUDO | Main.POR_PAPEL, function (concentrado, tickets, desmembramentos)
{
    var papel = null, acao, qtde_velha, saldo_velho, qtde_nova, preco_medio;
    var qtde_extinta, saldo_realizado, venda_acao;

    percorre_lancamentos(concentrado, function (L) {
        if (! L.daytrade) {
            if (papel === null) {
                papel = L.papel;
                acao = (tipo_papel(papel) != tipo_papel.OPCAO);
            }
    
            /* corretagem aumenta desde já o custo de aquisição */
            main.saldos_valor[papel] += L.corretagem;
    
            saldo_velho = main.saldos_valor[papel];
            qtde_velha = main.saldos_qtde[papel];
            preco_medio = qtde_velha === 0 ? 0 : (saldo_velho / qtde_velha);
    
            qtde_nova = main.saldos_qtde[papel] += L.qtde;
    
            qtde_extinta = saldo_realizado = 0;
    
            if (Math.abs(qtde_nova) > Math.abs(qtde_velha)) {
                // aumento do saldo absoluto
                // custo totalmente adicionado ao saldo
                main.saldos_valor[papel] += L.valor_total;
            } else {
                // diminuição do saldo absoluto
                // saldo diminui cfe. preço médio de formação
                qtde_extinta = L.qtde;
                saldo_realizado = Math.round(L.qtde * preco_medio);
                main.saldos_valor[papel] += saldo_realizado;
            }

            L.saldo_qtde = main.saldos_qtde[papel];
            L.saldo_valor = main.saldos_valor[papel];
            L.qtde_extinta = qtde_extinta;
            L.saldo_realizado = saldo_realizado;
            L.saldo_realizado_custo = qtde_extinta * L.valor_unitario;
            L._va20k = acao && (qtde_extinta < 0); /* venda de ações */
        }
    });
});


// calcula saldos de valor e realizações DT

rotina(Main.TUDO | Main.POR_PAPEL, function (concentrado, tickets, desmembramentos)
{
    var papel, qtde_velha, saldo_velho, qtde_nova, preco_medio;
    var qtde_extinta, saldo_realizado;
    var saldo_valor = 0, saldo_qtde = 0;

    percorre_lancamentos(concentrado, function (L) {
        if (L.daytrade) {
            papel = L.papel;
    
            /* corretagem aumenta desde já o custo de aquisição */
            saldo_valor += L.corretagem;
    
            saldo_velho = saldo_valor;
            qtde_velha = saldo_qtde;
            preco_medio = qtde_velha === 0 ? 0 : (saldo_velho / qtde_velha);
    
            qtde_nova = saldo_qtde += L.qtde;
    
            qtde_extinta = saldo_realizado = 0;
    
            if (Math.abs(qtde_nova) > Math.abs(qtde_velha)) {
                // aumento do saldo absoluto
                // custo totalmente adicionado ao saldo
                saldo_valor += L.valor_total;
            } else {
                // diminuição do saldo absoluto
                // saldo diminui cfe. preço médio de formação
                qtde_extinta = L.qtde;
                saldo_realizado = Math.round(L.qtde * preco_medio);
                saldo_valor += saldo_realizado;
            }
    
            L.saldo_qtde = saldo_qtde;
            L.saldo_valor = saldo_valor;
            L.qtde_extinta = qtde_extinta;
            L.saldo_realizado = saldo_realizado;
            L.saldo_realizado_custo = qtde_extinta * L.valor_unitario;
        }
    });
});

    
// Ganhos de capital, não-daytrade

rotina(Main.TUDO | Main.POR_PAPEL, function (concentrado, tickets, desmembramentos)
{
    percorre_lancamentos(concentrado, function (L) {
        if (! L.daytrade) {
            if (L.qtde_extinta !== 0) {
                // realização de lucros ou prejuízos
                // saldo_realizado: preço de custo que foi removido do saldo
                // saldo_realizado_custo: quanto custou remover a qtde extinta do saldo

                var lucro = L.saldo_realizado - L.saldo_realizado_custo;
                main.lucro_normal += lucro;

                if (L._va20k) {
                    var t = new Ticket(L.id, 0, L.data, L.hora, "@RV", lucro, 
                            0, "Realização "+L.papel+" venda ações", null);
                    t._vbru = -L.saldo_realizado_custo;
                    tickets.push(t);
                } else {
                    tickets.push(new Ticket(L.id, 0, L.data, L.hora, "@R", lucro, 
                            0, "Realização "+L.papel+" normal", null));
                }
                delete L._va20k;
            }
        }
    });
});


// Ganhos de capital, daytrade

rotina(Main.TUDO | Main.POR_PAPEL, function (concentrado, tickets, desmembramentos)
{
    percorre_lancamentos(concentrado, function (L) {
        if (L.daytrade) {
            if (L.qtde_extinta !== 0) {
                // realização de lucros ou prejuízos
                // saldo_realizado: preço de custo que foi removido do saldo
                // saldo_realizado_custo: quanto custou remover a qtde extinta do saldo

                var lucro = L.saldo_realizado - L.saldo_realizado_custo;
                main.lucro_daytrade += lucro;

                tickets.push(new Ticket(L.id, 0, L.data, L.hora, "@Rdt", lucro, 
                        0, "Realização "+L.papel+" daytrade", null));
            }
        }
    });
});


var limite_isencao = 2000000; /* centavos */

// Compila ganhos de capital para o fim do mês

rotina(Main.MENSAL, function (concentrado, tickets, desmembramentos)
{
    var lucro = 0, lucrov = 0, venda_bruta = 0;
    var data;

    percorre_tickets(concentrado, function (L) {
        if (L.papel == "@R") {
            lucro += L.valor;
            data = L.data;
        } else if (L.papel == "@RV") {
            lucrov += L.valor;
            venda_bruta += L._vbru;
            data = L.data;
        }
    });

    if (! data) {
        return;
    }

    data = ultimo_dia_mes(data);

    if (venda_bruta > limite_isencao || lucrov < 0) {
        lucro += lucrov;
        lucrov = 0;
    }

    if (lucro !== 0) {
        tickets.push(new Ticket(0, 0, data, 2359, "@L", lucro, 1, (lucro > 0 ? "Lucro" : "Prejuízo") + " normal do mês", null));
    }

    if (lucrov > 0) {
        tickets.push(new Ticket(0, 0, data, 2359, "@LV", lucrov, 1, "Lucro não tributável do mês", null));
    }
});


// Compila ganhos de capital para o fim do mês DT

rotina(Main.MENSAL, function (concentrado, tickets, desmembramentos)
{
    var lucro = 0;
    var data;
    percorre_tickets(concentrado, function (L) {
        if (L.papel == "@Rdt") {
            lucro += L.valor;
            data = L.data;
        }
    });

    if (! data) {
        return;
    }

    data = ultimo_dia_mes(data);
    if (lucro !== 0) {
        tickets.push(new Ticket(0, 0, data, 2359, "@Ldt", lucro, 1, (lucro > 0 ? "Lucro" : "Prejuízo") + " daytrade do mês", null));
    }
});


// Compila lucro tributável normal
// TODO epoch

var ir_normal = 15;
var ir_daytrade = 20;
var darf_minimo = 1000; /* 1000 centavos */

rotina(Main.TUDO, function (concentrado, tickets, desmembramentos)
{
    var lucro = 0;
    var data = null;
    percorre_tickets(concentrado, function (L) {
        if (data) {
            if (L.data > data) {
                if (lucro > 0) {
                    data = ultimo_dia_mes(data);
                    tickets.push(new Ticket(0, 0, data, 2359,  "@LT", lucro, 2, 
                            "Lucro tributável a " + ir_normal + "%", null));
                    lucro = 0;
                }
                
            }
        }
        if (L.papel == "@L") {
            lucro += L.valor;
            data = ultimo_dia_mes(L.data);
        }
    });
    if (lucro > 0) {
        data = ultimo_dia_mes(data);
        tickets.push(new Ticket(0, 0, data, 2359, "@LT", lucro, 2, "Lucro tributável a " + ir_normal + "%", null));
        lucro = 0;
    }
});

// Compila lucro tributável daytrade

rotina(Main.TUDO, function (concentrado, tickets, desmembramentos)
{
    var lucro = 0;
    var data = null;
    var ano = null;
    percorre_tickets(concentrado, function (L) {
        if (data) {
            if (L.data > data) {
                if (lucro > 0) {
                    data = ultimo_dia_mes(data);
                    tickets.push(new Ticket(0, 0, data, 2359, "@LTdt", lucro, 2, 
                            "Lucro tributável a " + ir_daytrade + "%", null));
                    lucro = 0;
                }
                
            }
            if (data_ano(L.data) != ano && lucro < 0) {
                // daytrade não leva prejuízo para próximo ano
                tickets.push(new Ticket(0, 0, data, 2359, "@Wdt", lucro, 2, 
                            "Prejuízo daytrade não mais compensável (virada ano)", null));
                lucro = 0;
            }
        }
        if (L.papel == "@Ldt") {
            lucro += L.valor;
            data = ultimo_dia_mes(L.data);
            ano = data_ano(L.data);
        }
    });
    if (lucro > 0) {
        data = ultimo_dia_mes(data);
        tickets.push(new Ticket(0, 0, data, 2359, "@LTdt", lucro, 2, "Lucro tributável a " + ir_daytrade + "%", null));
        lucro = 0;
    }
});

// Calcula imposto normal

rotina(Main.TUDO, function (concentrado, tickets, desmembramentos)
{
    var imposto = 0;
    percorre_tickets(concentrado, function (L) {
        if (L.papel == "@LT") {
            imposto += Math.round(L.valor * ir_normal / 100);
            main.imposto += imposto;
            if (imposto >= darf_minimo) {
                tickets.push(new Ticket(0, 0, ultimo_dia_util_proximo_mes(L.data), 2359, "@DARF", imposto, 3, "Imposto " + ir_normal + "% a pagar", null));
                imposto = 0;
            }
        }
    });
});

// Calcula imposto daytrade a pagar

rotina(Main.TUDO, function (concentrado, tickets, desmembramentos)
{
    var imposto = 0;
    percorre_tickets(concentrado, function (L) {
        if (L.papel == "@LTdt") {
            imposto += Math.round(L.valor * ir_normal / 100);
            main.imposto += imposto;
            if (imposto >= darf_minimo) {
                tickets.push(new Ticket(0, 0, ultimo_dia_util_proximo_mes(L.data), 2359, "@DARFdt", imposto, 3, "Imposto " + ir_daytrade + "% a pagar", null));
                imposto = 0;
            }
        }
    });
});

// Detecta posições em aberto de opções que já foram exintas

rotina(Main.TUDO | Main.POR_PAPEL, function (concentrado, tickets, desmembramentos)
{
    var vencimento = null;
    var saldo = 0;
    var papel = null;
    var now = agora();
    percorre_lancamentos(concentrado, function (L) {
        if (vencimento) {
            if (vencimento < L.data) {
                // movimentação após o vencimento e com saldo
                tickets.push(new Ticket(0, 0, now, 2359, "@EXP", 0, 4, 
                            "Opção "+papel+" com saldo expirado após "+dtoc(vencimento), null));
                saldo = 0;
                vencimento = null;
            }
        }
        
        if (papel === null) {
            papel = L.papel;
        }

        if (vencimento === null) {
            if (tipo_papel(papel) != tipo_papel.OPCAO) {
                return 1;
            }
            vencimento = vencimento_opcao(papel, L.data);
        }

        saldo += L.qtde;
        if (saldo === 0) {
            // posição zerada, esquece o vencimento
            vencimento = null;
        }

        return null;
    });

    if (vencimento !== null) {
        if (vencimento < now) {
            tickets.push(new Ticket(0, 0, now, 2359, "@EXP", 0, 4, 
                            "Opção "+papel+" com saldo expirado após "+dtoc(vencimento), null));
        }
    }
});


/*jslint browser: true, white: false */

function importar_fase2(txt)
{
    if (! main.vazio()) {
        if (prompt("A importação eliminará os lançamentos velhos.\nDigite SIM se você realmente deseja fazer isto", "Não") !== "SIM") {
            alert("Importação cancelada.");
            return;
        }
    }

    var res;

    try {
        res = $.secureEvalJSON(txt);
    } catch (exp) {
        res = null;
    }

    if (! res) {
        alert("Conteúdo inválido. Verifique se você colou o conteúdo completo do arquivo de cópia.");
    } else {
        main.ler_storage2(res);
        alert("Conteúdo importado com sucesso.");
    }
}

function importar_ff3()
{
    var arquivo = document.fimport.arquivo;
    var ok = 0;
    if (! arquivo.files) {
        alert("Este navegador não suporta leitura direta de arquivos. Siga a instrução genérica para qualquer browser.");
    }

    if (! arquivo.files.item(0)) {
        alert("Escolha o arquivo a ser importado.");
        return;
    }
    if (! arquivo.files.item(0).getAsBinary) {
        alert("Este navegador não suporta leitura direta de arquivos. Siga a instrução genérica para qualquer browser, mais abaixo");
    }
    var txt = arquivo.files.item(0).getAsBinary();
    if (! txt) {
        alert("Este navegador não suporta leitura direta de arquivos. Siga a instrução genérica para qualquer browser.");
    }

    importar_fase2(txt);
}

Main_class.exportar_fase1 = function ()
{
    var lista_exp = [];
    var elancto;

    for (var k = 0; k < this.lanctos.length; ++k) {
        lista_exp.push(this.lanctos[k].copia_para_exportacao());
    }

    document.fexport.txt.value = $.toJSON(lista_exp, 1) + "\r\n\r\n"; 
    document.fexport.arquivo.value = this.arquivo_salvo;
};

function exportar_fase2(e) {
    if (window.addEventListener) {
        var ev = document.createEvent("Event");
        ev.initEvent("TaxManSave", true, false);
        document.fexport.txt.dispatchEvent(ev);
    }
}

function exportar_feedback_ok(e)
{
    main.arquivo_salvo = document.fexport.arquivo.value;
    main.gravar_storage();
    alert("Arquivo salvo com sucesso.");
}

/*jslint browser: true, white: false */

var main;

var divlanctos, divposicao, divprocessamento;
var divnovidades;
var divinstrucoes;

var ANIM = 300;

var tela = {
        em_exibicao:    "Nenhum",
        em_modo:    "Nenhum", 
        err:        null,
        table_lanctos:  null,
        table_saldos:   null,
        saldos:     null,
        table_proc: null,
        addlanc:    null,
        addlanc_link:   null,
        divs:       [],
        links:      []
       };

function mostrar_addlanc()
{
    tela.addlanc.show(ANIM);
    tela.addlanc_link.hide(ANIM);
}

function esconder_addlanc()
{
    tela.addlanc.hide(ANIM);
    tela.addlanc_link.show(ANIM);
}

var links_iniciais = ["instrucoes", "importar", "novidades"];

function exibir(elemento_exibicao, carga)
{
    var modo = "normal";

    if (carga) {
        if (main.vazio()) {
            mostrar_addlanc();
        } else {
            esconder_addlanc();
        }
    }

    if (main.vazio()) {
        modo = "vazio";
        if (links_iniciais.indexOf(elemento_exibicao) < 0) {
            elemento_exibicao = links_iniciais[0];
        }
    }

    if (elemento_exibicao == tela.em_exibicao && modo == tela.em_modo) {
        return;
    }

    // links

    for(var k in tela.links) {
        if (k == elemento_exibicao) {
            tela.links[elemento_exibicao].hide(ANIM);
        } else if (links_iniciais.indexOf(k) >= 0) {
            tela.links[k].show(ANIM);
        } else if (! main.vazio()) {
            tela.links[k].show(ANIM);
        } else {
            tela.links[k].hide();
        }
    }

    // tabs

    var div_esconder = tela.divs[tela.em_exibicao];
    var div_mostrar = tela.divs[elemento_exibicao];

    if (div_esconder) {
        div_esconder.hide(ANIM);
    }
    div_mostrar.show(ANIM);

    tela.em_exibicao = elemento_exibicao;
    tela.em_modo = modo;

    if (elemento_exibicao == "exportar") {
        // joga dados para dentro do formulário de exportação
        main.exportar_fase1();
    }
}

function exportar_plugin_instalado(e)
{
    $("#firefox_semplugin").css('display', "none");
    $("#firefox_complugin").css('display', "block");
}

function instalar_validador(campo, decs, signal)
{
    var fformat = function(txt) {
        if (txt.length <= 0) {
            return "";
        }
        var sig = "0";
        if (signal) {
            if (txt.indexOf("-") >= 0) {
                if (txt.lastIndexOf("-") != txt.indexOf("-")) {
                    sig = "0";
                } else {
                    sig = "-0";
                }
            }
            if (txt.indexOf("+") >= 0) {
                sig = "0";
            }
        }
        var v = parseInt(sig + txt.replace(/[^0-9]/g, ''), 10);
        return formatFinancial(v, decs);
    };

    var ffinal = function () {
        var txt = $(this).val();
        $(this).val(fformat(txt));
    };

    var fpartial = function () {
        var txt = $(this).val();
        if (signal && (txt == "-" || txt == "+")) {
            return;
        }
        $(this).val(fformat(txt));
    };

    campo.change(ffinal);
    campo.keyup(fpartial);
}

function init_tela()
{
    tela.table_lanctos = $("#lanctos > tbody:first");
    tela.table_saldos = $("#saldos > tbody:first");
    tela.saldos = [
            $("#saldo_l1"), 
            $("#saldo_l2"), 
            $("#saldo_l3"),
            $("#saldo_l4") 
              ];
    tela.table_proc = $("#proc > tbody:first");
    tela.addlanc = $("#addlanc");
    tela.addlanc_link = $("#linkmostraaddlanc");

    // tela.notebook = $("#notebook");
    // tela.blanket = $("#blanket");

    var ids = ["lanctos", "posicao", "processamento", "novidades", "instrucoes", "importar", "exportar"];
    for(var k = 0; k < ids.length; ++k) {
        tela.links[ids[k]] = $("#link" + ids[k]);
        tela.divs[ids[k]] = $("#div" + ids[k]);
    }

    tela.err = $("#err");
}

function init_formulario()
{
    $('#form_data').datepicker({dateFormat: 'dd/mm/yy',
             monthNames: ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 
                          'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro',
                          'Novembro', 'Dezembro']});

    var h = new Date();
    h.setHours(10);
    h.setMinutes(0);
    $("#form_data").val(dtoc(jDtod(h)));
    $("#form_hora").val("10:00");
    $("#form_papel").val("VALE5");
    // $("#form_qtde").setValue(200);
    // $("#form_vlunit").setValue(30.05);
    // $("#form_corretagem").setValue(20);

    instalar_validador($("#form_qtde"), 0, true);
    instalar_validador($("#form_vlunit"), 2, false);
    instalar_validador($("#form_vltotal"), 2, false);
    instalar_validador($("#form_corretagem"), 2, false);
}

function init_extensao_firefox()
{
    if (window.addEventListener) {
        // comunicação com add-on Firefox de salvamento
        addEventListener("TaxManSaveAck", exportar_feedback_ok, false);
        addEventListener("TaxManSavePong", exportar_plugin_instalado, false);

        var ev = document.createEvent("Event");
        ev.initEvent("TaxManSavePing", true, false);
        document.fexport.txt.dispatchEvent(ev);
    }
}
/*jslint browser: true, white: false */

jQuery.jStore.ready(function(engine) {
    jQuery.jStore.flashReady(function(){
        engine.ready(function() {
            main.ler_storage(this);
        });
    });
    engine.ready(function() {
        main.ler_storage(this);
    });
}); 

$(document).ready(function() {
    init_tela();
    init_formulario();
    init_extensao_firefox();
    main = new Main(); // singleton
    jQuery.extend(jQuery.jStore.defaults,
              {project: 'taxman',
               flash: 'jStore.Flash.html'});
    jQuery.jStore.load();
});
