%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net>
%% 
%% Permission is hereby granted, free of charge, to any person
%% obtaining a copy of this software and associated documentation
%% files (the "Software"), to deal in the Software without
%% restriction, including without limitation the rights to use,
%% copy, modify, merge, publish, distribute, sublicense, and/or sell
%% copies of the Software, and to permit persons to whom the
%% Software is furnished to do so, subject to the following
%% conditions:
%% 
%% The above copyright notice and this permission notice shall be
%% included in all copies or substantial portions of the Software.
%% 
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
%% OTHER DEALINGS IN THE SOFTWARE.
%%
%% @doc A module for creating nice looking code coverage reports.
-module(etap_report).
-export([create/0]).

%% @spec create() -> ok
%% @doc Create html code coverage reports for each module that code coverage
%% data exists for.
create() ->
    [cover:import(File) || File <- filelib:wildcard("cover/*.coverdata")],
    Modules = lists:foldl(
        fun(Module, Acc) ->
            [{Module, file_report(Module)} | Acc]
        end,
        [],
        cover:imported_modules()
    ),
    index(Modules).

%% @private
index(Modules) ->
    {ok, IndexFD} = file:open("cover/index.html", [write]),
    io:format(IndexFD, "<html><head><style>
    table.percent_graph { height: 12px; border:1px solid #E2E6EF; empty-cells: show; }
    table.percent_graph td.covered { height: 10px; background: #00f000; }
    table.percent_graph td.uncovered { height: 10px; background: #e00000; }
    .odd { background-color: #ddd; }
    .even { background-color: #fff; }
    </style></head>", []),
    io:format(IndexFD, "<body>", []),
    lists:foldl(
        fun({Module, {Good, Bad, Source}}, LastRow) ->
            case {Good + Bad, Source} of
                {0, _} -> LastRow;
                {_, none} -> LastRow;
                _ ->
                    CovPer = round((Good / (Good + Bad)) * 100),
                    UnCovPer = round((Bad / (Good + Bad)) * 100),
                    RowClass = case LastRow of 1 -> "odd"; _ -> "even" end,
                    io:format(IndexFD, "<div class=\"~s\">", [RowClass]),
                    io:format(IndexFD, "<a href=\"~s\">~s</a>", [atom_to_list(Module) ++ "_report.html", atom_to_list(Module)]),
                    io:format(IndexFD, "
                    <table cellspacing='0' cellpadding='0' align='right'>
                      <tr>
                        <td><tt>~p%</tt>&nbsp;</td><td>
                          <table cellspacing='0' class='percent_graph' cellpadding='0' width='100'>
                          <tr><td class='covered' width='~p' /><td class='uncovered' width='~p' /></tr>
                          </table>
                        </td>
                      </tr>
                    </table>
                    ", [CovPer, CovPer, UnCovPer]),
                    io:format(IndexFD, "</div>", []),
                    case LastRow of
                        1 -> 0;
                        0 -> 1
                    end
            end
        end,
        0,
        lists:sort(Modules)
    ),
    {TotalGood, TotalBad} = lists:foldl(
        fun({_, {Good, Bad, Source}}, {TGood, TBad}) ->
            case Source of none -> {TGood, TBad}; _ -> {TGood + Good, TBad + Bad} end
        end,
        {0, 0},
        Modules
    ),
    io:format(IndexFD, "<p>Generated on ~s.</p>~n", [etap:datetime({date(), time()})]),
    case TotalGood + TotalBad of
        0 -> ok;
        _ ->
            TotalCovPer = round((TotalGood / (TotalGood + TotalBad)) * 100),
            TotalUnCovPer = round((TotalBad / (TotalGood + TotalBad)) * 100),
            io:format(IndexFD, "<div>", []),
            io:format(IndexFD, "Total 
            <table cellspacing='0' cellpadding='0' align='right'>
              <tr>
                <td><tt>~p%</tt>&nbsp;</td><td>
                  <table cellspacing='0' class='percent_graph' cellpadding='0' width='100'>
                  <tr><td class='covered' width='~p' /><td class='uncovered' width='~p' /></tr>
                  </table>
                </td>
              </tr>
            </table>
            ", [TotalCovPer, TotalCovPer, TotalUnCovPer]),
            io:format(IndexFD, "</div>", [])
    end,
    io:format(IndexFD, "</body></html>", []),
    file:close(IndexFD),
    ok.

%% @private
file_report(Module) ->
    {ok, Data} = cover:analyse(Module, calls, line),
    Source = find_source(Module),
    {Good, Bad} = collect_coverage(Data, {0, 0}),
    case {Source, Good + Bad} of
        {none, _} -> ok;
        {_, 0} -> ok;
        _ ->
            {ok, SourceFD} = file:open(Source, [read]),
            {ok, WriteFD} = file:open("cover/" ++ atom_to_list(Module) ++ "_report.html", [write]),
            io:format(WriteFD, "~s", [header(Module, Good, Bad)]),
            output_lines(Data, WriteFD, SourceFD, 1),
            io:format(WriteFD, "~s", [footer()]),
            file:close(WriteFD),
            file:close(SourceFD),
            ok
    end,
    {Good, Bad, Source}.

%% @private
collect_coverage([], Acc) -> Acc;
collect_coverage([{{_, _}, 0} | Data], {Good, Bad}) ->
    collect_coverage(Data, {Good, Bad + 1});
collect_coverage([_ | Data], {Good, Bad}) ->
    collect_coverage(Data, {Good + 1, Bad}).

%% @private
output_lines(Data, WriteFD, SourceFD, LineNumber) ->
    {Match, NextData} = datas_match(Data, LineNumber),
    case io:get_line(SourceFD, '') of
        eof -> ok;
        Line = "%% @todo" ++ _ ->
            io:format(WriteFD, "~s", [out_line(LineNumber, highlight, Line)]),
            output_lines(NextData, WriteFD, SourceFD, LineNumber + 1);
        Line = "% " ++ _ ->
            io:format(WriteFD, "~s", [out_line(LineNumber, none, Line)]),
            output_lines(NextData, WriteFD, SourceFD, LineNumber + 1);
        Line ->
            case Match of
                {true, CC} ->
                    io:format(WriteFD, "~s", [out_line(LineNumber, CC, Line)]),
                    output_lines(NextData, WriteFD, SourceFD, LineNumber + 1);
                false ->
                    io:format(WriteFD, "~s", [out_line(LineNumber, none, Line)]),
                    output_lines(NextData, WriteFD, SourceFD, LineNumber + 1)
            end
    end.

%% @private
out_line(Number, none, Line) ->
    PadNu = string:right(integer_to_list(Number), 5, $.),
    io_lib:format("<span class=\"marked\"><a name=\"line~p\"></a>~s ~s</span>", [Number, PadNu, Line]);
out_line(Number, highlight, Line) ->
    PadNu = string:right(integer_to_list(Number), 5, $.),
    io_lib:format("<span class=\"highlight\"><a name=\"line~p\"></a>~s ~s</span>", [Number, PadNu, Line]);
out_line(Number, 0, Line) ->
    PadNu = string:right(integer_to_list(Number), 5, $.),
    io_lib:format("<span class=\"uncovered\"><a name=\"line~p\"></a>~s ~s</span>", [Number, PadNu, Line]);
out_line(Number, _, Line) ->
    PadNu = string:right(integer_to_list(Number), 5, $.),
    io_lib:format("<span class=\"covered\"><a name=\"line~p\"></a>~s ~s</span>", [Number, PadNu, Line]).

%% @private
datas_match([], _) -> {false, []};
datas_match([{{_, Line}, CC} | Datas], LineNumber) when Line == LineNumber -> {{true, CC}, Datas};
datas_match(Data, _) -> {false, Data}.

%% @private
find_source(Module) when is_atom(Module) ->
    Root = filename:rootname(Module),
    Dir = filename:dirname(Root),
    XDir = case os:getenv("SRC") of false -> "src"; X -> X end,
    find_source([
        filename:join([Dir, Root ++ ".erl"]),
        filename:join([Dir, "..", "src", Root ++ ".erl"]),
        filename:join([Dir, "src", Root ++ ".erl"]),
        filename:join([Dir, "elibs", Root ++ ".erl"]),
        filename:join([Dir, "..", "elibs", Root ++ ".erl"]),
        filename:join([Dir, XDir, Root ++ ".erl"])
    ]);
find_source([]) -> none;
find_source([Test | Tests]) ->
    case filelib:is_file(Test) of
        true -> Test;
        false -> find_source(Tests)
    end.

%% @private
header(Module, Good, Bad) ->
    io:format("Good ~p~n", [Good]),
    io:format("Bad ~p~n", [Bad]),
    CovPer = round((Good / (Good + Bad)) * 100),
    UnCovPer = round((Bad / (Good + Bad)) * 100),
    io:format("CovPer ~p~n", [CovPer]),
    io_lib:format("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">
        <html lang='en' xml:lang='en' xmlns='http://www.w3.org/1999/xhtml'>
          <head>
            <title>~s - C0 code coverage information</title>
            <style type='text/css'>body { background-color: rgb(240, 240, 245); }</style>
            <style type='text/css'>span.marked0 {
     background-color: rgb(185, 210, 200);
     display: block;
    }
    span.marked { display: block; background-color: #ffffff; }
    span.highlight { display: block; background-color: #fff9d7; }
    span.covered { display: block; background-color: #f7f7f7 ; }
    span.uncovered { display: block; background-color: #ffebe8 ; }
    span.overview {
     border-bottom: 1px solid #E2E6EF; 
    }
    div.overview {
     border-bottom: 1px solid #E2E6EF; 
    }
    body {
     font-family: verdana, arial, helvetica;
    }
    div.footer {
     font-size: 68%;
     margin-top: 1.5em;
    }
    h1, h2, h3, h4, h5, h6 {
     margin-bottom: 0.5em;
    }
    h5 {
     margin-top: 0.5em;
    }
    .hidden {
     display: none;
    }
    div.separator {
     height: 10px;
    }
    table.percent_graph {
     height: 12px;
     border: 1px solid #E2E6EF; 
     empty-cells: show;
    }
    table.percent_graph td.covered {
     height: 10px;
     background: #00f000;
    }
    table.percent_graph td.uncovered {
     height: 10px;
     background: #e00000;
    }
    table.percent_graph td.NA {
     height: 10px;
     background: #eaeaea;
    }
    table.report {
     border-collapse: collapse;
     width: 100%;
    }
    table.report td.heading {
     background: #dcecff;
     border: 1px solid #E2E6EF; 
     font-weight: bold;
     text-align: center;
    }
    table.report td.heading:hover {
     background: #c0ffc0;
    }
    table.report td.text {
     border: 1px solid #E2E6EF; 
    }
    table.report td.value {
     text-align: right;
     border: 1px solid #E2E6EF; 
    }
    table.report tr.light {
     background-color: rgb(240, 240, 245);
    }
    table.report tr.dark {
     background-color: rgb(230, 230, 235);
    }
    </style>
          </head>
          <body>
            <h3>C0 code coverage information</h3>
            <p>Generated on ~s with <a href='http://github.com/ngerakines/etap'>etap 0.3.4</a>.
            </p>            
        <table class='report'>
          <thead>
            <tr>
              <td class='heading'>Name</td>
              <td class='heading'>Total lines</td>
              <td class='heading'>Lines of code</td>
              <td class='heading'>Total coverage</td>
              <td class='heading'>Code coverage</td>
            </tr>
          </thead>
          <tbody>
            <tr class='light'>

              <td>
                <a href='~s'>~s</a>
              </td>
              <td class='value'>
                <tt>??</tt>
              </td>
              <td class='value'>
                <tt>??</tt>
              </td>
              <td class='value'>
                <tt>??</tt>
              </td>
              <td>
                <table cellspacing='0' cellpadding='0' align='right'>
                  <tr>
                    <td><tt>~p%</tt>&nbsp;</td><td>
                      <table cellspacing='0' class='percent_graph' cellpadding='0' width='100'>
                      <tr><td class='covered' width='~p' /><td class='uncovered' width='~p' /></tr>
                      </table>
                    </td>
                  </tr>
                </table>
              </td>
            </tr>
          </tbody>
        </table><pre>", [Module, etap:datetime({date(), time()}), atom_to_list(Module) ++ "_report.html", Module, CovPer, CovPer, UnCovPer]).

%% @private
footer() ->
    "</pre><hr /><p>Generated using <a href='http://github.com/ngerakines/etap'>etap 0.3.4</a>.</p>
          </body>
        </html>
    ".