#!/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(),
    IEDir = filename:join(Wings_dir, "plugins/import_export"),
    code:add_patha(IEDir),
    Opts0 = scan_format(IEDir),
    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);
	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) ->
    wings_pref:init(),   %% Preference ets table is needed
    wings_file:init(),   %% more preferences
    wings_image:init(wings_not_running), %% Needed for image export
    wings_color:init(),  %% Needed for materials
    wings_plugin:init(), %% Needed for image export
    [import_file(File, Opts) || 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) ->
    parse_args(Rest, Opts#opts{verbose=true});
parse_args(["--verbose"|Rest], Opts) ->
    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], 
    erlang: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.

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

import_file(File, Opts) ->
    put(wings_not_running, {import, File}),
    verbose("~s => ", [File], Opts#opts.verbose),
    try import_file_1(File,Opts) of	
	{error,Reason} ->
	    io:format("**** Import Failed: ~p On file: ~p~n~n", [Reason, File]),
	    halt(1);
	Wings when element(1, Wings) =:= st ->
	    export_file(Wings, filename:rootname(filename:basename(File)), Opts)
    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, _) ->
    St0 = wings:new_st(),
    wings_ff_wings:import(File, St0);
import_file_2(#format{mod=ndo}, File, _) ->
    St0 = wings:new_st(),
    wings_ff_ndo:import(File, St0);
import_file_2(#format{mod=Mod, ext_type=Type, option=false},
	      _File, _Opts) when is_atom(Mod) ->
    St0 = wings:new_st(),
    Mod:command({file,{import,Type}}, St0);
import_file_2(#format{mod=Mod, ext_type=Type, option=true}, 
	      _File, #opts{modify=Modify}) when is_atom(Mod) ->
    St0 = wings:new_st(),
    Mod:command({file,{import,{Type,Modify}}}, St0);

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.

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

export_file(WingsData, File, Opts=#opts{dir=Dir, out_module=F=#format{ext_type=Ext}}) ->
    FileName = filename:join(Dir, File++"."++ atom_to_list(Ext)),
    verbose("~s~n", [FileName], Opts#opts.verbose),
    put(wings_not_running, {export, FileName}),
    try export_file_1(F, FileName, WingsData, 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, WingsData, Opts) ->
    export_file_2(F, FileName, WingsData, Opts);
export_file_1(F, FileName, WingsData, Opts = #opts{modify=Modify}) ->
    export_file_2(F, FileName, modify_model(Modify, WingsData), Opts).

export_file_2(#format{mod=wings}, FileName, WingsData, _Opts) ->
    wings_ff_wings:export(FileName, WingsData);
export_file_2(#format{mod=nendo}, FileName, WingsData, _Opts) ->
    wings_ff_ndo:export(FileName, WingsData);
export_file_2(#format{mod=Mod, ext_type=Type, option=false}, 
	      _FileName, WingsData, _Opts) ->    
    Mod:command({file,{export,Type}}, WingsData); 
export_file_2(#format{mod=Mod, ext_type=Type, option=true}, 
	      _FileName, WingsData, #opts{modify=Modify}) ->
    Mod:command({file,{export,{Type,Modify}}}, WingsData).

verbose(_,_, false) -> ok;
verbose(F,A, true) ->
    io:format(F,A).

modify_model([], WingsData) ->
    WingsData;
modify_model(Ps, St) ->
    SubDivs = proplists:get_value(subdivisions, Ps, 0),
    Tess = proplists:get_value(tesselation, Ps, none),
    
    wings_we:map(fun(We0) -> 
			 We1 = sub_divide(SubDivs, We0),
			 We2 = wings_we:show_faces(We1),
			 tesselate(Tess, We2)
		 end, St).

sub_divide(0, We) -> We;
sub_divide(N, We0) ->
    We = wings_subdiv:smooth(We0),
    sub_divide(N-1, We).

tesselate(none, We) -> We;
tesselate(triangulate, We) ->
    Fs = wings_we:visible(We),
    wings_tesselation:triangulate(Fs, We);
tesselate(quadrangulate, We) ->
    Fs = wings_we:visible(We),
    wings_tesselation:quadrangulate(Fs, We).
