#!/usr/bin/env escript
%%   -*- erlang -*-
%%     Wings 3D File convertion.
%%
%%  Copyright (c) 2010 Dan Gudmundsson
%%
%%  See the file "license.terms" for information on usage and redistribution
%%  of this file, and for a DISCLAIMER OF ALL WARRANTIES.
%%

-mode(compile).

%% If moved outside of wings directory modify
-define(WINGS_DIR, "c:/src/wings/ebin").

-record(opts,
	{dir = ".",       %% Ouput to directory
	 out_module,      %% Output format
	 verbose=false,   %% Verbose output
	 in_format,       %% In format (if unknown extension).
	 image_format,    %% Image out format
	 in_formats=[],   %% Scanned, all import formats
	 out_formats=[],  %% Scanned, all export formats
	 modify=[]        %% Convertion modifications
	}).

-record(format,
	{mod,             %% Module
	 ext_type,        %% Extension
	 str="",          %% Description string
	 option=false     %% Allows options
	}).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

main(Args) ->
    Wings_dir = setup_paths(),
    ok = wings:start(),
    Init = fun() ->
                   wxTopLevelWindow:iconize(wings_wm:get_value(top_frame)),
                   keep
           end,
    wings ! {action, Init},
    IEDir = filename:join(Wings_dir, "plugins/import_export"),
    code:add_patha(IEDir),
    Opts0 = scan_format([IEDir]),
    put(verbose, false),
    case parse_args(Args, Opts0) of
	{#opts{out_module=undefined},_} ->
	    io:format("**** Error:  Out format not specified~n~n"),
	    usage(Opts0);
	{Opts = #opts{}, Files} ->
	    convert(Files, Opts),
            erlang:halt(0);
	error ->
	    usage(Opts0)
    end.

setup_paths() ->
    Escript   = filename:dirname(filename:absname(escript:script_name())),
    EnvDir    = os:getenv("WINGS_DIR"),
    DefDir    = ?WINGS_DIR,
    case test_paths([Escript, EnvDir,DefDir]) of
	{ok, Path} ->
	    code:add_patha(filename:join([Path, "ebin"])),
	    Path;
	_ ->
	    io:format("**** Error:  Compiled wings files not found~n~n"),
	    io:format("             use 'set WINGS_DIR=c:\PATH_TO_WINGS_INSTALL~n~n")
    end.

test_paths([false|Rest]) -> test_paths(Rest);
test_paths([Path0|Rest]) ->
    Path = strip_path(lists:reverse(Path0)),
    case filelib:is_regular(filename:join([Path, "ebin", "wings.beam"])) of
	true  -> {ok, Path};
	false -> test_paths(Rest)
    end;
test_paths([]) -> not_found.

strip_path("nibe/" ++ Path) -> lists:reverse(Path);
strip_path("crs/"  ++ Path) -> lists:reverse(Path);
strip_path(Path)            -> lists:reverse(Path).

convert(Fs, Opts) ->
    Convert = fun(File) ->
                      call_wings({file, confirmed_new}),
                      ok = import_file(File, Opts),
                      ok = do_mods(Opts),
                      Out = filename:rootname(filename:basename(File)),
                      export_file(Out, Opts)
              end,
    [Convert(File) || File <- Fs],
    ok.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

scan_format(Dir) ->
    Files = filelib:wildcard(filename:join(Dir, "wpc_*.beam")),
    Plugin = fun(File, {Type, Acc}) ->
		     Mod = list_to_atom(filename:rootname(filename:basename(File))),
		     case Mod:menu({file, Type}, []) of
			 [{Str, ExtT, Extra}] ->
			     F = #format{mod=Mod, ext_type=ExtT, str = strip(Str),
					 option = lists:member(option, Extra)
					},
			     {Type,[F|Acc]};
			 [{Str, ExtT}] ->
			     F = #format{mod=Mod, ext_type=ExtT, str = strip(Str)},
			     {Type,[F|Acc]};
			 [] ->
			     {Type, Acc}
		     end
	     end,
    Default = [#format{mod=nendo, ext_type=ndo, str="Nendo (.ndo)"},
	       #format{mod=wings, ext_type=wings, str="Wings (.wings)"}],
    {_,Export} = lists:foldl(Plugin, {export, Default}, Files),
    {_,Import} = lists:foldl(Plugin, {import, Default}, Files),

    #opts{in_formats=Import, out_formats=Export}.

strip(Str) ->
    strip_1(lists:reverse(Str)).

strip_1([$.|Rest]) ->
    strip_1(Rest);
strip_1(Str) ->
    lists:reverse(Str).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

parse_args(["-o", Dir|Rest], Opts) ->
    parse_args(Rest, Opts#opts{dir=Dir});
parse_args(["--outdir", Dir|Rest], Opts) ->
    parse_args(Rest, Opts#opts{dir=Dir});
parse_args(["-v"|Rest], Opts) ->
    put(verbose, true),
    parse_args(Rest, Opts#opts{verbose=true});
parse_args(["--verbose"|Rest], Opts) ->
    put(verbose, true),
    parse_args(Rest, Opts#opts{verbose=true});

parse_args(["--subdiv", N0|Rest], Opts=#opts{modify=Mod}) ->
    N = try
	    list_to_integer(N0)
	catch _:_ ->
		io:format("**** Error: Option --subdiv ~p Not an integer ~n~n", [N0]),
		usage(Opts)
	end,
    parse_args(Rest, Opts#opts{modify=[{subdivisions, N}|Mod]});
parse_args(["--tess"++_, "tri"++_|Rest], Opts=#opts{modify=Mod}) ->
    parse_args(Rest, Opts#opts{modify=[{tesselation,triangulate}|Mod]});
parse_args(["--tess"++_, "quad"++_|Rest], Opts=#opts{modify=Mod}) ->
    parse_args(Rest, Opts#opts{modify=[{tesselation,quadrangulate}|Mod]});

parse_args(["--informat", Format|Rest], Opts) ->
    parse_args(Rest, Opts#opts{in_format=check_format(in, Format, Opts)});
parse_args(["-f", Format|Rest], Opts) ->
    parse_args(Rest, Opts#opts{out_module=check_format(out, Format, Opts)});
parse_args([Opt=[$-|_]| _], Opts) ->
    io:format("**** Error:  Unknown option ~p~n~n", [Opt]),
    usage(Opts);
parse_args(Files, Opts) ->
    {Opts, Files}.

check_format(Dir, Ext = [A|_], Opts) when A =/= $. ->
    check_format(Dir, [$.|Ext], Opts);
check_format(in, Ext, O=#opts{in_formats=In}) ->
    case get_module(Ext, In) of
	error ->
	    check_format_err(in, Ext, O);
	Mod -> Mod
    end;
check_format(out, Ext, O=#opts{out_formats=Out}) ->
    case get_module(Ext, Out) of
	error ->
	    check_format_err(out, Ext, O);
	Mod -> Mod
    end.

check_format_err(Dir, Format, Opts) ->
    io:format("**** Error:  Format ~p for ~pput is not supported ~n~n", [Format,Dir]),
    usage(Opts).

usage(#opts{in_formats=In, out_formats=Out}) ->
    io:format("Usage: wings_convert -f OutFormat [Opts] Files ~n"
	      "  Converts between file formats. ~n"
	      "  Output is written to the current directory by default.~n~n"
	      " Options:~n"
	      "   -o, --outdir DIR       Write converted files to DIR.~n"
	      "   -v, --verbose          Verbose output.~n"
	      "   --informat FORMAT      Ignore file extension and use FORMAT as input.~n"
	      "   --subdiv N             Subdivide object N times (default 0).~n"
	      "   --tess TYPE            Tesselate object none|tri|quad (default none)~n"

%%	      "   --image                Convert images"
	      "~n"
	     ),
    io:format("~nSupported import formats:~n",[]),
    [io:format("  ~s~n", [Str]) || #format{str=Str} <- In],
    io:format("~nSupported export formats:~n",[]),
    [io:format("  ~s~n", [Str]) || #format{str=Str} <- Out],
    io:nl(),
    halt(1).

get_module(Ext, [F=#format{str=Str}|List]) ->
    case string:str(Str,Ext) of
	0 ->
	    get_module(Ext,List);
	_ ->
	    F
    end;
get_module(_, []) ->
    error.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

call_wings(Cmd) ->
    verbose("Script: ~p~n",[{action, Cmd}]),
    wings ! {action, Cmd},
    Me = self(),
    Sync = fun(_St) ->
                   Me ! {cmd, sync},
                   keep
           end,
    wings ! {external, Sync},
    receive {cmd, sync} -> ok end,
    ok.

import_file(File, Opts) ->
    verbose("~s => ~n", [File]),
    wings ! {action, fun() -> put(wings_not_running, {import, File}), keep end},
    try import_file_1(File,Opts) of
	{error,Reason} ->
	    io:format("**** Import Failed: ~p On file: ~p~n~n", [Reason, File]),
	    halt(1);
	ok ->
            ok
    catch
	_:{command_error,Message} ->
	    io:format("**** Import Failed: ~s On file: ~p~n~n", [Message, File]),
	    halt(1);
	_:Reason ->
	    io:format("**** Import crashed: ~p On file: ~p~n~n", [Reason, File]),
	    io:format("Debug info: ~p~n~n",[erlang:get_stacktrace()]),
	    halt(1)
    end.

import_file_1(File, Opts=#opts{in_format=undefined}) ->
    import_file_2(filename:extension(File),File,Opts);

import_file_1(File, Opts=#opts{in_format=InFormat}) ->
    import_file_2(InFormat,File,Opts).

import_file_2(#format{mod=wings}, File, _) ->
    call_wings({file, {confirmed_open, File}});
import_file_2(#format{mod=nendo}, File, _) ->
    call_wings({file, {import, {ndo, File}}});
import_file_2(#format{ext_type=Type, option=false}, _File, _Opts)  ->
    call_wings({file,{import,Type}});
import_file_2(#format{ext_type=Type, option=true}, _File, _) ->
    call_wings({file,{import,{Type,[]}}});

import_file_2(Str, File, Opts = #opts{in_formats=In}) ->
    case get_module(Str, In) of
	error ->
	    io:format("**** Error:  Import Failed: ~p On file: ~p~n~n",
		      ["Unknown import format", File]),
	    halt(1);
	Mod -> import_file_2(Mod, File, Opts)
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
do_mods(#opts{modify=Mods}) ->
    modify_model(Mods).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

export_file(File, Opts=#opts{dir=Dir, out_module=F=#format{ext_type=Ext}}) ->
    FileName = filename:join(Dir, File++"."++ atom_to_list(Ext)),
    verbose("Export to: ~s~n", [FileName]),
    wings ! {action, fun() -> put(wings_not_running, {export, FileName}), keep end},

    try export_file_1(F, FileName, Opts) of
	{error,Reason} ->
	    io:format("**** Export Failed: ~p On file: ~p~n~n", [Reason, FileName]),
	    halt(1);
	ok ->
	    ok
    catch
	_:{command_error,Message} ->
	    io:format("**** Export Failed: ~s On file: ~p~n~n", [Message, File]),
	    halt(1);
	_:Reason ->
	    io:format("**** Export crashed: ~p On file: ~p~n~n", [Reason, FileName]),
	    io:format("Debug info: ~p~n~n",[erlang:get_stacktrace()]),
	    halt(1)
    end.

export_file_1(F=#format{option=true}, FileName, Opts) ->
    export_file_2(F, FileName, Opts);
export_file_1(F, FileName, Opts) ->
    export_file_2(F, FileName, Opts).

export_file_2(#format{mod=wings}, FileName, _Opts) ->
    call_wings({file, {save_as, {FileName, ignore}}});
export_file_2(#format{mod=nendo}, FileName, _Opts) ->
    call_wings({file, {export, {ndo, FileName}}});
export_file_2(#format{ext_type=Type, option=false}, _FN, _Opts) ->
    call_wings({file,{export,Type}});
export_file_2(#format{ext_type=Type, option=true}, _F, _Opts) ->
    Mods = [{include_uvs, true}, {include_normals, true}],
    call_wings({file,{export,{Type, Mods}}}).

verbose(F,A) ->
    get(verbose) andalso io:format(F,A).

modify_model([]) ->
    ok;
modify_model(Ps) ->
    SubDivs = proplists:get_value(subdivisions, Ps, 0),
    Tess = proplists:get_value(tesselation, Ps, none),
    call_wings({select, body}),
    call_wings({select, all}),
    sub_divide(SubDivs),
    tesselate(Tess),
    ok.

sub_divide(0) -> ok;
sub_divide(N) ->
    call_wings({body, smooth}),
    sub_divide(N-1).

tesselate(none) -> ok;
tesselate(triangulate) ->
    call_wings({select, face}),
    call_wings({face, {tesselate, triangulate}});
tesselate(quadrangulate) ->
    call_wings({select, face}),
    call_wings({face, {tesselate, quadrangulate}}).
