% vim: ts=4 sw=4 expandtab

:- module off.
% read in an off object and put it in the standard form:  i.e. scale it so it
% just fits in a 1x1x1 cube and centre it at the origin.

:- interface.

:- import_module io, int, list, vec3, gfx.

:- type off_object --->
        obj(
                list(vec3),     % list of vertices
                list(off_face)   % list of faces
           ).

:- type off_face --->
        face(
                int,            % number of vertices
                list(int),      % list of vertex indices
                colour          % intrinsic colour of face
            ).

% read_off(Filename, Object)
% read Object from Filename and ``standardise'' it
:- pred read_off(string, off_object, io__state, io__state).
:- mode read_off(in, out, di, uo) is det.

:- implementation.

:- import_module char, require, string, split_line, float, debug.

read_off(Filename, Object) -->
    (
        io__open_input(Filename, Result),
            (
                { Result = error(ErrorCode) },
                { io__error_message(ErrorCode, ErrorString) },
                { error(ErrorString) }
            ;
                { Result = ok(Stream) },
                io__set_input_stream(Stream, OldStream), % set input stream to the off file
                read_header(NumVertices, NumFaces),
                read_vertices(NumVertices, Vertices0),
                read_faces(NumFaces, Faces),
                { standardise(Vertices0, Vertices) },
                { Object = obj(Vertices, Faces) },
                io__set_input_stream(OldStream, _), % set input stream back to the scene file
                io__close_input(Stream)
            )
    ).

% read_header(NumVertices, NumFaces)
% read the off file header return the number of vertices and faces
:- pred read_header(int, int, io__state, io__state).
:- mode read_header(out, out, di, uo) is det.

read_header(NumVertices, NumFaces) -->
    (
        get_next_word(Word0),
            (
                % detect optional "OFF" keyword at start of file
                { Word0 = "OFF" }
            -> 
                get_next_word(Word1)
            ;
                { Word1 = Word0 }
            ),
            (
                { string__to_int(Word1, X1) }
            ->
                { NumVertices = X1 }
            ;
                io__input_stream_name(Filename),
                { string__append(Filename, ": Error reading OFF header", ErrorString) },
                { error(ErrorString) }
            ),
        get_next_word(Word2),
            (
                { string__to_int(Word2, X2) }
            ->
                { NumFaces = X2 }
            ;
                io__input_stream_name(Filename),
                { string__append(Filename, ": Error reading OFF file", ErrorString1) },
                { error(ErrorString1) }
            ),
        get_next_word(_)    % read in and ignore number of edges
    ).

% read in vertices
:- pred read_vertices(int, list(vec3), io__state, io__state).
:- mode read_vertices(in, out, di, uo).

read_vertices(N, L) -->
    (
        { N =< 0 }
    ->
        { L = [] }
    ;
        get_next_float(X),
        get_next_float(Y),
        get_next_float(Z),
        { V = vec(X, Y, Z) },
        read_vertices(N - 1, L1),
        { L = [V | L1] }
    ).

% read in next space-delimited word and convert to a float
:- pred get_next_float(float, io__state, io__state).
:- mode get_next_float(out, di, uo) is det.

get_next_float(F) -->
    (
        get_next_word(W),
            (
                { string__to_float(W, F1) }
            ->
                { F = F1 }
            ;
                io__input_stream_name(FileName),
                { string__append(FileName, ": Error reading OFF vertex list", ErrorString) },
                { error(ErrorString) }
            )
    ).

% get next space-delimited word from the OFF file, strip comments and return a
% string.  Call error/1 if string can't be read in.
:- pred get_next_word(string, io__state, io__state).
:- mode get_next_word(out, di, uo) is det.

get_next_word(Word) -->
        get_next_word0(CharList),
        { string__from_char_list(CharList, Word) }.

:- pred get_next_word0(list(char), io__state, io__state).
:- mode get_next_word0(out, di, uo) is det.

get_next_word0(L) -->
    (
        io__read_word(Result),
            (
                { Result = eof },
                io__input_stream_name(Filename),
                { string__append(Filename, ": OFF file too short", ErrorString) },
                { error(ErrorString) }
            ;
                { Result = error(ErrorCode) },
                { io__error_message(ErrorCode, ErrorString1) },
                { error(ErrorString1) }
            ;
                { Result = ok(L0) },
                strip_comments_from_word(L0, L1),
                    (
                        { L1 = [] }
                    -> 
                        get_next_word0(L)
                    ;
                        { L = L1 }
                    )
            )
    ).

:- pred strip_comments_from_word(list(char), list(char), io__state, io__state).
:- mode strip_comments_from_word(in, out, di, uo) is det.

strip_comments_from_word(Lin, Lout) -->
    (
        { Lin = [] },
        { Lout = [] }
    ;
        { Lin = [C | L0] },
            (
                { C = '#'}
            ->
                { Lout = [] },
                io__read_line(_) % discard rest of line
            ;
                strip_comments_from_word(L0, L1),
                { Lout = [C | L1] }
            )
    ).
            
% read_faces(NumFaces, Faces)
% read in the faces from the OFF file
% each line of the file contains 1 face
:- pred read_faces(int, list(off_face), io__state, io__state).
:- mode read_faces(in, out, di, uo) is det.

read_faces(N, Fs) -->
    (
        { N =< 0}
    ->
        { Fs = [] }
    ;
        io__read_line(Result),
            (   
                { Result = eof },
                io__input_stream_name(Filename),
                { string__append(Filename, ": OFF file too short", ErrorString) },
                { error(ErrorString) }
            ;
                { Result = error(ErrorCode) },
                { io__error_message(ErrorCode, ErrorString1) },
                { error(ErrorString1) }
            ;
                { Result = ok(L0) },
                io__input_stream_name(Filename),
                { strip_comments_from_line(L0, L1) },
                { split_line(L1, Words0) },
                    (
                        % check for a blank line
                        { Words0 = [] },
                        read_faces(N, Fs)
                    ;
                        { Words0 = [W1 | Words1] },
                        %{ dump("off.m:read_faces W1 **%s**", [s(W1)]) }, %% dmo
                            (
                                { W1 = ""}
                            ->
                                read_faces(N, Fs)
                            ;
                                (
                                    { string__to_int(W1, N1) }
                                ->
                                    { NumVerts = N1 },
                                    %{ dump("off.m:read_faces about to call get_vertex_list for %d vertices\n", [i(NumVerts)]) }, %dmo
                                    { get_vertex_list(NumVerts, Filename, VertList, Words1, Words2) },
                                    { get_colour(Words2, Colour) },
                                    { P = face(NumVerts, VertList, Colour) },
                                    read_faces(N - 1, Fs1),
                                    { Fs = [P | Fs1] }
                                ;
                                    { string__append(Filename, ": Error reading OFF faces", ErrorString2) },
                                    { error(ErrorString2) }
                                )
                            )
                    )
            )
    ).

% strip comments from a line of the file (list of chars)
:- pred strip_comments_from_line(list(char), list(char)).
:- mode strip_comments_from_line(in, out) is det.

strip_comments_from_line(Lin, Lout) :-
    (
        Lin = [],
        Lout = []
    ;
        Lin = [C | L1],
            (
                C = '#'
            ->
                Lout = []
            ;
                strip_comments_from_line(L1, L2),
                Lout = [C | L2]
            )
    ).

% get the list of vertex indices for a face
% filename is passed in for error reporting
:- pred get_vertex_list(int, string, list(int), list(string), list(string)).
:- mode get_vertex_list(in, in, out, in, out) is det.

get_vertex_list(N, Filename, Vs, WordsIn, WordsOut) :-
    (
        N =< 0
    ->
        Vs = [],
        WordsOut = WordsIn
    ;
        (
            WordsIn = [],
            string__append(Filename, ": Error reading OFF faces", ErrorString),
            error(ErrorString)
        ;
            WordsIn = [W | Ws1],
                (
                    string__to_int(W, I)
                ->
                    %dump("off.m:get_vertex_list: found vertex %d\n", [i(I)]), %dmo
                    get_vertex_list(N - 1, Filename, Vs1, Ws1, WordsOut),
                    Vs = [I | Vs1]
                ;
                    string__append(Filename, ": Error reading OFF faces", ErrorString1),
                    error(ErrorString1)
                )
        )
    ).
                    

% get colour for face
:- pred get_colour(list(string), colour).
:- mode get_colour(in, out) is det.

get_colour(Words, Colour) :-
    (
        Words = [W1, W2, W3 | _]
    ->
        (
            string__to_int(W1, I1), 0 =< I1, I1 =< 255,
            string__to_int(W2, I2), 0 =< I2, I2 =< 255,
            string__to_int(W3, I3), 0 =< I3, I3 =< 255
        ->
            F1a = float(I1),
            F2a = float(I2),
            F3a = float(I3),
            Colour = colour(F1a/255.0, F2a/255.0, F3a/255.0)
        ;
            string__to_float(W1, F1), 0.0 =< F1, F1 =< 1.0,
            string__to_float(W2, F2), 0.0 =< F2, F2 =< 1.0,
            string__to_float(W3, F3), 0.0 =< F3, F3 =< 1.0
        ->
            Colour = colour(F1, F2, F3)
        ;
            % if can't convert properly, use default colour
            Colour = default_colour    
        )
    ;
        Words = [W1 | _]
    ->
        % if a single number is given, try to use it as a greyscale value
        (
            string__to_int(W1, I1), 0 =< I1, I1 =< 255
        ->
            F1a = float(I1),
            F1b = F1a/255.0,
            Colour = colour(F1b, F1b, F1b)
        ;
            string__to_float(W1, F1), 0.0 =< F1, F1 =< 1.0
        ->
            Colour = colour(F1, F1, F1)
        ;
            Colour = default_colour
        )
    ;
        Colour = default_colour
    ).

% transform the object into standard form.  IE transform to fit into a 1x1 cube
% centred at the origin.
:- pred standardise(list(vec3), list(vec3)).
:- mode standardise(in, out) is det.

standardise(VLin, VLout) :-
    (
        get_min_max(VLin, MinX, MaxX, MinY, MaxY, MinZ, MaxZ),
        % dump("get_min_max results: %f %f %f %f %f %f\n", [f(MinX), f(MaxX), f(MinY), f(MaxY), f(MinZ), f(MaxZ)]), % debug - dmo
        Max1 = float__max(MaxX - MinX, MaxY - MinY),
        MaxExtent = float__max(Max1, MaxZ - MinZ),
        veclist_translate(VLin, vec(-(MinX+MaxX)/2.0, -(MinY+MaxY)/2.0, -(MinZ+MaxZ)/2.0), VL1),
        ( MaxExtent = 0.0 -> error("Max Extent is 0!!") ; true ),
        veclist_scale(VL1, 1.0/MaxExtent, VLout)
    ).

% find min and max extents in each direction
:- pred get_min_max(list(vec3), float, float, float, float, float, float).
:- mode get_min_max(in, out, out, out, out, out, out) is det.

get_min_max(VLin, MinX, MaxX, MinY, MaxY, MinZ, MaxZ) :-
    (
        VLin = [], 
        MinX = float__max,
        MinY = float__max,
        MinZ = float__max,
        MaxX = float__min,
        MaxY = float__min,
        MaxZ = float__min
    ;
        VLin = [vec(X, Y, Z) | VL1],
        %dump("get_min_max: current vector %f %f %f\n", [f(X), f(Y), f(Z)]),  % debug - dmo
        get_min_max(VL1, MinX1, MaxX1, MinY1, MaxY1, MinZ1, MaxZ1),
        MinX = float__min(X, MinX1),
        MinY = float__min(Y, MinY1),
        MinZ = float__min(Z, MinZ1),
        MaxX = float__max(X, MaxX1),
        MaxY = float__max(Y, MaxY1),
        MaxZ = float__max(Z, MaxZ1)
    ).

% translate all vectors in list
:- pred veclist_translate(list(vec3), vec3, list(vec3)).
:- mode veclist_translate(in, in, out) is det.

veclist_translate(VLin, VT, VLout) :-
    (
        VLin = [],
        VLout = []
    ;
        VLin = [V1 | VL1],
        V2 = V1 + VT,
        veclist_translate(VL1, VT, VL2),
        VLout = [V2 | VL2]
    ).

% scale all vectors in list
:- pred veclist_scale(list(vec3), float, list(vec3)).
:- mode veclist_scale(in, in, out) is det.

veclist_scale(VLin, Scale, VLout) :-
    (
        VLin = [],
        VLout = []
    ;
        VLin = [V1 | VL1],
        V2 = scale(Scale, V1),
        veclist_scale(VL1, Scale, VL2),
        VLout = [V2 | VL2]
    ).
