%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 1996-2009. All Rights Reserved.
%% 
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%% 
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%% 
%% %CopyrightEnd%
%%

%%
-module(cols).

-export([start/0, init/0]).

%% internal export.
-export([make_board_elem/3]).

%%======================================================================
%% Contents
%%=====================
%% 1. The actual program
%% 2. Graphics
%% 3. Data structures and stuff
%% 4. Lambdas
%%======================================================================


-define(COLORS, {red,green,blue,grey,yellow,{66,153,130}}).
-define(HIGHFILE, "./cols.high").
-define(HEIGHT, 17).
-define(LEFT, 50).
-define(SIZE, 15).
-define(VERSION, "v0.9").
-define(WIDTH, 8).

-record(state, {bit,board,nextbit,ticks, score=0}).
%%----------------------------------------------------------------------
%% Consists of three boxes.
%%----------------------------------------------------------------------
-record(bit, {x,y,topColor, middleColor, bottomColor,
	      top_gsobj,mid_gsobj,bot_gsobj}).

%%======================================================================
%% 1. The actual program
%%======================================================================

start() ->
    spawn_link(cols,init,[]).

init() ->
    make_graphics(),
    {A,B,C} = erlang:now(),
    random:seed(A,B,C),
    NextBit = make_bit(),
    Board = make_screen_board(),
    S = #state{bit=make_bit(), board=Board, ticks=update_timer(1),
	       score=make_score(), nextbit=new_bit_xy(NextBit, -2,5)},
    gs:config(win, [{map, true}]),
    loop(S).

make_graphics() ->
    G = gs:start(),
    H = ?HEIGHT*?SIZE,
    W = ?WIDTH*?SIZE,
    BotMargin = 100,
    gs:create(window, win, G, [{destroy,true},{map, true},{title, "cols"},
			       {height, H+BotMargin}, {width, W+?LEFT+10},
			       {bg, grey},{keypress,true}]),
    gs:create(canvas, can, win, [{bg, black},
				 {height, H+BotMargin},
				 {width, W+?LEFT+20}]),
    gs:create(text, can, [{text, "Next"}, {coords, [{5, 45}]}, {fg, red}]),
    gs:create(image, help, can, [{coords,[{5,7}]},
				 {load_gif, dir() ++ "/help.gif"},
				 {buttonpress,true}]),
    draw_borders().

loop(State) ->
    receive
	Event -> loop(update(Event, State))
    end.

%%----------------------------------------------------------------------
%% How fast speed should be doubled
%%----------------------------------------------------------------------
-define(DBL_TICKS, 300).

update_timer(Ticks) ->
    K = 0.001/?DBL_TICKS,
    M = 1.001-K,
    Q = K*Ticks+M,
    Timeout = round(1/math:log(Q)),
    timer:send_after(Timeout, self(), fall_timeout),
    Ticks+1.

add_score({ScoreObj, NScore}, DScore) ->
    NScore2 = NScore + DScore,
    gs:config(ScoreObj, [{text, io_lib:format("Score: ~w", [NScore2])}]),
    {ScoreObj, NScore2}.
    

update({gs,_Obj,keypress,_Data, ['Left'|_]}, State) ->
    #state{bit=Bit, board = Board} = State,
    #bit{x=X,y=Y} = Bit,
    if X > 0 -> 
	    case is_board_empty(Board, X-1,Y) of
		true ->
		    State#state{bit=new_bit_xy(Bit, X-1, Y)};
		false ->
		    State
	    end;
	true -> State
    end;

update({gs,_Obj,keypress,_Data, ['Right'|_]}, State) ->
    #state{bit=Bit, board = Board} = State,
    #bit{x=X,y=Y} = Bit,
    if X < ?WIDTH - 1 ->
	    case is_board_empty(Board, X+1, Y) of
		true ->
		    State#state{bit=new_bit_xy(Bit, X+1, Y)};
		false ->
		    State
	    end;
	true -> State
    end;

update({gs,_Obj,keypress,_Data, ['Up'|_]}, State) ->
    State#state{bit=shift_bits(State#state.bit)};    

update({gs,_Obj,keypress,_Data, [Key|_]}, State) ->
    case drop_key(Key) of
	true ->
	    #state{bit=Bit, board=Board, score=Score} = State,
	    #bit{x=X,y=Y} = Bit,
	    {NewX, NewY, NewScore} = drop(X,Y,Score,Board),
	    fasten_bit(State#state{bit=new_bit_xy(Bit,NewX, NewY),
				   score=NewScore});
	false -> State
    end;
	
update(fall_timeout, State) ->
    #state{bit=Bit, board=Board, ticks = Ticks, score=Score} = State,
    NewY = Bit#bit.y+1,
    X = Bit#bit.x,
    case is_fall_ok(Board, X, NewY) of
	true ->
	    State#state{bit=new_bit_xy(Bit, X, NewY),
			ticks=update_timer(Ticks), score=add_score(Score, 1)};
	false ->
	    S1 = fasten_bit(State),
	    S1#state{ticks=update_timer(Ticks)}
    end;

update({gs,_,destroy,_,_}, _State) ->
    exit(normal);

update({gs,help,buttonpress,_,_}, State) ->
    show_help(),
    State;

update(OtherEvent, State) ->
    ok=io:format("got other! ~w~n", [OtherEvent]), State.
 
drop_key('Down') -> true;
drop_key(space) -> true;
drop_key(_) -> false.

is_board_empty(Board, X, Y) ->
    case {color_at(Board, X, Y),
	  color_at(Board, X, Y + 1),
	  color_at(Board, X, Y + 2)} of
	{black, black, black} -> true;
	_ -> false
    end.

%%----------------------------------------------------------------------
%% Returns: NewState
%%----------------------------------------------------------------------
fasten_bit(State) ->
    #state{board=Board, bit=Bit, nextbit=NextBit, score=Score} = State,
    #bit{x=X,y=Y,topColor=C1,middleColor=C2,bottomColor=C3} = Bit,
    B1 = update_screen_element(Board, X, Y, C1),
    B2 = update_screen_element(B1, X, Y+1, C2),
    B3 = update_screen_element(B2, X, Y+2, C3),
    destroy_bit(Bit),
    #bit{topColor=NC1,middleColor=NC2,bottomColor=NC3} = NextBit,
    {B4, ExtraScore} = erase_bits(B3, [{X,Y},{X,Y+1},{X,Y+2}], 0),
    NewBit = make_bit(NC1,NC2,NC3),
    case is_board_empty(B4, NewBit#bit.x, NewBit#bit.y) of
	true ->
	    State#state{score=add_score(Score, ExtraScore),
			bit=NewBit, nextbit=new_colors(NextBit),board=B4};
	false ->
	    {_GsObj,Score2}=State#state.score,
	    highscore:run(Score2,?HIGHFILE),
	    exit(normal)
    end.

%%----------------------------------------------------------------------
%% Args: Check: list of {X,Y} to check.
%% Returns: {NewBoard, ExtraScore}
%%----------------------------------------------------------------------
erase_bits(Board, Checks, ExtraScore) ->
    ElemsToDelete = elems2del(Checks,Board,[]),
    NDel = length(ElemsToDelete),
    if
	NDel > 0 ->
	    Board2 = delete_elems(Board, ElemsToDelete),
	    {NewBoard, NewCheck} = fall_down(Board2, ElemsToDelete),
	    if NDel > 3 -> 
		    {B,ES}=erase_bits(NewBoard,NewCheck,ExtraScore+2*NDel),
		    {NewBoard2, NewCheck2} = bonus(B, NewCheck),
		    erase_bits(NewBoard2, NewCheck2, ES);
		true -> 
		    erase_bits(NewBoard, NewCheck, 2*NDel)
	    end;
	true -> {Board, ExtraScore}
    end.

bonus(Board, Check) ->
    Cols = collect_bottom_bits(0,Board),
    NewBoard = randomize_columns(5, Board, Cols),
    NewCheck = update_check(Check, Cols),
    {NewBoard, NewCheck}.

randomize_columns(0, Board, _) -> Board;
randomize_columns(N, Board, Cols) ->
    NewBoard = randomize_columns(Cols,Board),
    randomize_columns(N-1, NewBoard, Cols).
			
randomize_columns([],Board) -> Board; 
randomize_columns([X|Xs],Board) ->
    flush(),
    timer:sleep(50),
    randomize_columns(Xs,update_screen_element(Board,X,?HEIGHT-1,rndColor())).

%%----------------------------------------------------------------------
%% Returns: NewBoard
%%----------------------------------------------------------------------
delete_elems(Board, Elems2Del) ->
    OrgObjs = org_objs(Elems2Del,Board),
    visual_effect(?SIZE, OrgObjs),
    NewBoard = update_board(Elems2Del, Board),
    put_back(OrgObjs),
    NewBoard.

visual_effect(0,_OrgObjs) -> done;
visual_effect(Size,OrgObjs) ->
    set_size(OrgObjs,Size),
    flush(),
    timer:sleep(20),
    visual_effect(Size-1,OrgObjs).

set_size([],_Size) -> done;
set_size([{GsObj,[{X1,Y1},{_X2,_Y2}]}|T],Size) ->
    gs:config(GsObj, [{coords, [{X1,Y1},{X1+Size,Y1+Size}]}]),
    set_size(T,Size).

%%----------------------------------------------------------------------
%% Note: Loop over columns where something is removed only. (efficiency)
%% Returns: {ReversedNewColumns (perhaps shorter), Checks}
%% cols:fall_column([a,b,black,black,c,f,black,d,black], 3, 15, [], []).
%% should return: {[a,b,c,f,d],[{3,11},{3,12},{3,13}]}
%%----------------------------------------------------------------------
fall_column([], _X, _Y, ColumnAcc, ChecksAcc) ->
    {ColumnAcc, ChecksAcc};
fall_column([black|Colors], X, Y, ColumnAcc, ChecksAcc) ->
    case find_box(Colors) of
	false -> {ColumnAcc, ChecksAcc};
	NewColors when list(NewColors) ->
		fall_one_step(NewColors, X, Y, ColumnAcc, ChecksAcc)
    end;
fall_column([Color|Colors], X, Y, ColumnAcc, ChecksAcc) ->
    fall_column(Colors, X, Y-1, [Color | ColumnAcc], ChecksAcc).

find_box([]) -> false;
find_box([black|Colors]) ->
    find_box(Colors);
find_box([Color|Colors]) -> [Color|Colors].
    
%%----------------------------------------------------------------------
%% Enters: ([a,b, , ,c,d], 3, 8, Q) 
%% Leaves: ([b,a|Q], [ , , ,c,d], 10, [{3,8},{4,9}])
%%----------------------------------------------------------------------
fall_one_step([], X, Y, ColumnAcc, Checks) ->
    fall_column([], X, Y, ColumnAcc, Checks);
fall_one_step([black|Colors], X, Y, ColumnAcc, Checks) ->
    fall_column([black|Colors], X, Y, ColumnAcc, Checks);
fall_one_step([Color|Colors], X, Y, ColumnAcc, Checks) ->
    fall_one_step(Colors, X, Y-1, [Color|ColumnAcc],[{X,Y}|Checks]).

%%----------------------------------------------------------------------
%% Returns: {NewBoard, NewChecks}
%%----------------------------------------------------------------------
fall_down(Board1, Elems2Del) ->
    UpDatedCols = updated_cols(Elems2Del, []),
    fall_column(UpDatedCols, Board1, []).

fall_column([], NewBoard, NewChecks) -> {NewBoard, NewChecks};
fall_column([X|Xs], BoardAcc, ChecksAcc) ->
    OrgColumn = boardcolumn_to_tuple(BoardAcc, X),
    Column = columntuple_to_list(OrgColumn),
    {NewColumn, NewChecksAcc} = fall_column(Column, X,?HEIGHT-1,[],ChecksAcc),
    NewBoardAcc =
	set_board_column(BoardAcc,X,new_column_list(NewColumn,OrgColumn)),
    fall_column(Xs,NewBoardAcc,NewChecksAcc).

new_column_list(NewColumn, ColumnTuple) ->
    Nempty = ?HEIGHT - length(NewColumn),
    L = make_list(black, Nempty) ++ NewColumn,
    new_column_list(L, 1, ColumnTuple).

new_column_list([H|T], N, Tuple) ->
    {GsObj, Color} = element(N, Tuple),
    [update_screen_element({GsObj, Color},H) | new_column_list(T, N+1, Tuple)];
new_column_list([], _, _) -> [].
    

%%----------------------------------------------------------------------
%% Returns: a reversed list of colors.
%%----------------------------------------------------------------------
columntuple_to_list(ColumnTuple) when tuple(ColumnTuple) ->
    columntuple_to_list(tuple_to_list(ColumnTuple),[]).

columntuple_to_list([],Acc) -> Acc;
columntuple_to_list([{_GsObj, Color}|T],Acc) ->
    columntuple_to_list(T,[Color|Acc]).

%%======================================================================
%% 2. Graphics
%%======================================================================

make_bit() ->
    make_bit(rndColor(),rndColor(),rndColor()).

make_bit(Tc,Mc,Bc) ->
    X = ?WIDTH div 2,
    Y = 0,
    #bit{x=X,y=Y,topColor= Tc, middleColor=Mc, bottomColor=Bc,
	top_gsobj = make_box(X,Y,Tc), mid_gsobj=make_box(X,Y+1,Mc),
	bot_gsobj=make_box(X,Y+2,Bc)}.
 
new_colors(Bit) ->
    #bit{top_gsobj=T,mid_gsobj=M,bot_gsobj=B} = Bit,
    Tc = rndColor(),
    Mc = rndColor(),
    Bc = rndColor(),
    gs:config(T, [{fill, Tc}]),
    gs:config(M, [{fill, Mc}]),
    gs:config(B, [{fill, Bc}]),
    Bit#bit{topColor= Tc, middleColor=Mc, bottomColor=Bc}.

new_bit_xy(Bit, NewX, NewY) ->
    #bit{x=X,y=Y,top_gsobj=T,mid_gsobj=M,bot_gsobj=B} = Bit,
    Dx = (NewX - X) * ?SIZE,
    Dy = (NewY - Y) * ?SIZE,
    gs:config(T, [{move, {Dx, Dy}}]),
    gs:config(M, [{move, {Dx, Dy}}]),
    gs:config(B, [{move, {Dx, Dy}}]),
    Bit#bit{x=NewX, y=NewY}.

destroy_bit(#bit{top_gsobj=T,mid_gsobj=M,bot_gsobj=B}) ->
    gs:destroy(T),
    gs:destroy(M),
    gs:destroy(B).

shift_bits(Bit) ->
    #bit{topColor=C1,middleColor=C2,bottomColor=C3,
	 top_gsobj=T,mid_gsobj=M,bot_gsobj=B} = Bit,
    gs:config(T, {fill,C2}),
    gs:config(M, {fill,C3}),
    gs:config(B, {fill,C1}),
    Bit#bit{topColor=C2, middleColor=C3, bottomColor=C1}.

rndColor() ->
    Siz = size(?COLORS),
    element(random:uniform(Siz), ?COLORS).

make_score() ->
    {gs:create(text, can, [{text, "Score: 0"}, {fg, red},
			   {coords, [{5,?HEIGHT*?SIZE+10}]}]), 0}.

make_screen_board() ->
    xy_loop({cols,make_board_elem}, make_board(), ?WIDTH, ?HEIGHT).

make_board_elem(X,Y,Board) ->
    set_board_element(Board,X,Y,{make_box(X,Y,black),black}).

flush() -> gs:read(can, bg).

draw_borders() ->
    BotY = ?HEIGHT*?SIZE,
    RightX = ?LEFT + ?SIZE*?WIDTH,
    LeftX = ?LEFT - 1,
    gs:create(line,can,[{coords,[{LeftX,0},{LeftX,BotY}]},{fg,white}]),
    gs:create(line,can,[{coords,[{LeftX,BotY},{RightX,BotY}]},{fg,white}]),
    gs:create(line,can,[{coords,[{RightX,0},{RightX, BotY}]}, {fg,white}]).

update_screen_element(ScrBoard, X, Y, Color) ->
    case board_element(ScrBoard,X,Y) of
	{_GsObj, Color} ->
	    ScrBoard; % don't have to update screen
	{GsObj, _ScreenColor} ->
	    gs:config(GsObj, color_args(Color)),
	    set_board_element(ScrBoard, X, Y, {GsObj, Color})
    end.

update_screen_element(ScrElem, Color) ->
    case ScrElem of
	{_GsObj, Color} ->
	    ScrElem; % don't have to update screen
	{GsObj, _ScreenColor} ->
	    gs:config(GsObj, color_args(Color)),
	    {GsObj, Color}
    end.
    

color_args(black) -> [{fg,black},{fill,black}];
color_args(Color) -> [{fg,white},{fill,Color}].

%%======================================================================
%% 3. Data structures and stuff
%%======================================================================

xy_loop(Fun, Acc,  XMax, YMax) ->
    xy_loop(Fun, Acc, 0, 0, XMax, YMax).

xy_loop(_Fun, Acc, _X, YMax, _XMax, YMax) -> Acc;
xy_loop(Fun, Acc, XMax, Y, XMax, YMax) ->
    xy_loop(Fun, Acc, 0, Y+1, XMax, YMax);
xy_loop(Fun, Acc, X, Y, XMax, YMax) ->
    xy_loop(Fun, apply(Fun, [X, Y,Acc]), X+1,Y,XMax, YMax).

%%----------------------------------------------------------------------
%% Returns: a sorted list of {X,Y} to delete.
%% Pre: PrevDelElems is sorted.
%%----------------------------------------------------------------------
erase_bits_at(Board, PrevDelElems, X,Y) ->
    C = color_at(Board, X, Y),
    erase_bits_at([vert, horiz, slash, backslash],X,Y,C,Board,PrevDelElems).
    
erase_bits_at([], _X,_Y,_C,_Board, Elems2Del) -> Elems2Del;
erase_bits_at([Dir|Ds],X,Y,C,Board, Elems2DelAcc) ->
    Dx = dx(Dir),
    Dy = dy(Dir),
    DelElems = lists:append(check_dir(Board, X-Dx,Y-Dy,-Dx,-Dy,C),
			    check_dir(Board, X,Y,Dx,Dy,C)),
    N_in_a_row = length(DelElems),
    if N_in_a_row >= 3 ->
	    erase_bits_at(Ds,X,Y,C,Board,
			  ordsets:union(lists:sort(DelElems),Elems2DelAcc));
       true -> erase_bits_at(Ds,X,Y,C,Board,Elems2DelAcc)
    end.

dx(vert) -> 0;
dx(horiz) -> 1;
dx(slash) -> 1;
dx(backslash) -> -1.

dy(vert) -> -1;
dy(horiz) -> 0;
dy(slash) -> -1;
dy(backslash) -> -1.


%%----------------------------------------------------------------------
%% Returns: list of {X,Y} to delete.
%%----------------------------------------------------------------------
check_dir(Board, X, Y, Dx, Dy, Color) 
  when X >= 0, X < ?WIDTH, Y >= 0, Y < ?HEIGHT ->
    case color_at(Board, X, Y) of
	Color ->
	    [{X,Y} | check_dir(Board, X+Dx, Y+Dy, Dx, Dy, Color)];
	_OtherColor ->
            []
    end;
check_dir(_Board, _X, _Y, _Dx, _Dy, _Color) -> [].
  
make_box(X, Y, Color) ->
    make_box(X, Y, 1, 1, Color).

%%----------------------------------------------------------------------
%% Returns: GsObj
%%----------------------------------------------------------------------
make_box(X, Y, Height, Width, Color) ->
    Opts = if Color == black -> [{fg, black}, {fill, black}];
	       true -> [{fill, Color}, {fg, white}] end,
    gs:create(rectangle, can, [{coords, [{?LEFT + X * ?SIZE, Y * ?SIZE},
					 {?LEFT + X * ?SIZE + (?SIZE*Width)-1,
					 Y * ?SIZE + (?SIZE*Height)-1}]}|Opts]).

is_fall_ok(_Board, _NewX, NewY) when NewY+2 >= ?HEIGHT -> false;
is_fall_ok(Board, NewX, NewY) ->
    case color_at(Board, NewX, NewY+2) of
	black ->
	    true;
	_ -> false
    end.

color_at(Board, X, Y) ->
    {_GsObj, Color} = board_element(Board, X, Y),
    Color.


%%----------------------------------------------------------------------
%% X:0..?WIDTH-1, Y:0..?HEIGHT
%%----------------------------------------------------------------------
make_board() ->
    list_to_tuple(make_list(make_column(), ?WIDTH)).

board_element(Board, X, Y) ->
    element(Y+1, element(X+1, Board)).

set_board_element(Board, X, Y, NewValue) ->
    Col = element(X+1, Board),
    NewCol=setelement(Y+1,Col, NewValue),
    setelement(X+1, Board, NewCol).

make_column() ->
    list_to_tuple(make_list(black, ?HEIGHT)).

make_list(_Elem, 0) -> [];
make_list(Elem, N) -> [Elem|make_list(Elem,N-1)].

boardcolumn_to_tuple(Board, X) ->
    element(X+1, Board).

set_board_column(Board, X, NewCol) when length(NewCol) == ?HEIGHT ->
    setelement(X+1, Board, list_to_tuple(NewCol)).

show_help() ->
    W = gs:create(window, win, [{title, "cols Help"}, {width, 300},
				{height,300}, {map, true}]),
    gs:create(label, W, [{x,0},{y,0},{height, 200},{width,300},{justify,center},
			 {label, {text, 
    "cols $Revision: 1.23 $" 
    "\nby\n"
    "Klas Eriksson, eklas@erlang.ericsson.se\n\n"
    "Help: Use arrows and space keys.\n"
    "      Try to get 3 in-a-row.\n"
    "      More than 3 gives bonus."}}]),
    B=gs:create(button, W, [{x,100},{y,250}, {label, {text, "Dismiss"}}]),
    receive
	{gs, B, click, _, _} -> ok
    end,
    gs:destroy(W).

%%======================================================================
%% 4. Lambdas
%%======================================================================

drop(X,Y,Score,Board) ->
    case is_fall_ok(Board, X, Y+1) of
	true -> drop(X,Y+1,add_score(Score, 1),Board);
	false -> {X,Y, Score}
    end.

elems2del([], _Board,Elems2DelAcc) -> Elems2DelAcc;
elems2del([{X,Y}|Checks],Board,Elems2DelAcc) ->
    NewElems2DelAcc = ordsets:union(erase_bits_at(Board,Elems2DelAcc,X,Y),
				    Elems2DelAcc),
    elems2del(Checks,Board,NewElems2DelAcc).

collect_bottom_bits(?WIDTH,_Board) -> [];
collect_bottom_bits(X,Board) ->
    case color_at(Board, X, ?HEIGHT-1) of
	black -> collect_bottom_bits(X+1,Board);
	_AcolorHere -> [X|collect_bottom_bits(X+1,Board)]
    end.

update_check(_Check,[]) -> [];
update_check(Check,[X|Xs]) ->
    case lists:member({X, ?HEIGHT-1}, Check) of
	true -> update_check(Check,Xs);
	false -> [{X, ?HEIGHT-1}|update_check(Check,Xs)]
    end.

org_objs([],_Board) -> [];
org_objs([{X,Y}|XYs],Board) ->
    {GsObj, _Color} = board_element(Board, X, Y),
    [{GsObj, lists:sort(gs:read(GsObj, coords))}|org_objs(XYs,Board)].

update_board([],Board) -> Board;
update_board([{X,Y}|XYs], Board) ->
    update_board(XYs,update_screen_element(Board, X, Y, black)).

put_back([]) -> done;
put_back([{GsObj, Coords}|Objs]) ->
    gs:config(GsObj, [{coords, Coords}]),
    put_back(Objs).

updated_cols([], UpdColsAcc) -> UpdColsAcc;
updated_cols([{X,_Y}|XYs], UpdColsAcc) ->
    case lists:member(X,UpdColsAcc) of
	true -> updated_cols(XYs,UpdColsAcc);
	false -> updated_cols(XYs,[X|UpdColsAcc])
    end.

%% This is not an application so we don't have their way of knowing
%% a private data directory where the GIF files are located (this directory).
%% We can find GS and makes it relative from there /kgb

-define(EbinFromGsPriv,"../contribs/ebin").

dir()->
    GsPrivDir = code:priv_dir(gs),
    filename:join(GsPrivDir,?EbinFromGsPriv).
