/*
    Copyright 2003 Matthew Marjanovic <maddog@mir.com>

    This file is part of y4mscaler.

    y4mscaler is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    y4mscaler is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with y4mscaler; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

#include "y4m-config.h"
#include "debug.h"


#include "ysScaling.H"
#include "scaler-matto.H"

#include <string.h>


void ysScaling::_create_factory_list(void)
{
  _factory_count = 2;
  _factory_list = new factory_info[_factory_count];

  _factory_list[0].id = "default";
  _factory_list[0].factory = new mattoScalerFactory();

  _factory_list[1].id = "matto";
  _factory_list[1].factory = new mattoScalerFactory();
}


void ysScaling::_destroy_factory_list(void)
{
  if (_factory_list != NULL) {
    for (int i = 0; i < _factory_count; i++) {
      delete _factory_list[i].factory;
    }
    delete[] _factory_list;
    _factory_list = NULL;
  }
}


ysScaling::ysScaling(void) : 
  _factory(NULL),
  _mono(0),
  _line_switching(0),
  _swap_ilace(0),
  _scalers(NULL),
  _scaler_count(0)
{
  _create_factory_list();
}


ysScaling::~ysScaling()
{
  _destroy_factory_list();
  _factory = NULL;
  destroy_scalers();
}



void ysScaling::require_factory(void) {
  if (_factory == NULL)
    set_factory(_factory_list[0].factory);
}



void ysScaling::describe_keywords(FILE *fp, const char *prefix) const 
{
  fprintf(fp, "%smode=mono\n", prefix);
  fprintf(fp, "%smode=lineswitch\n", prefix);
  fprintf(fp, "%sscaler=scaler-name\n", prefix);
  fprintf(fp, "%s  Available scalers:\n", prefix);
  int maxlen = 0;
  for (int i = 0; i < _factory_count; i++) {
    int x = strlen(_factory_list[i].id);
    if (x > maxlen) maxlen = x;
  }
  for (int i = 0; i < _factory_count; i++) {
    fprintf(fp, "%s    '%s'%*s - %s %s\n", prefix,
	    _factory_list[i].id, maxlen - (int)strlen(_factory_list[i].id), "",
	    _factory_list[i].factory->description(),
	    (i==0 ? "(default)" : ""));
  }
  fprintf(fp, "%soption=scaler-option\n", prefix);
  fprintf(fp, "%s  Use 'option=help' to see options for chosen scaler.\n", prefix);
}



void ysScaling::parse_keyword(char *optarg)
{
  if (!strcasecmp(optarg, "MODE=MONO")) {
    _mono = 1;

  } else if (!strcasecmp(optarg, "MODE=LINESWITCH")) {
    _line_switching = 1;

  } else if (!strncasecmp(optarg, "SCALER=", 7)) {
    
    if (_factory != NULL) {
      mjpeg_error_exit1("A scaler has already been chosen: '%s'", optarg);
    }
    for (int i = 0; i < _factory_count; i++) {
      if (!strcasecmp(optarg+7, _factory_list[i].id)) {
	set_factory(_factory_list[i].factory);
	break;
      }
    }
    if (_factory == NULL) {
      mjpeg_error_exit1("Unrecognized scaler selected:  '%s'", optarg);
    }

  } else if (!strncasecmp(optarg, "OPTION=", 7)) {

    char *option = optarg+7;
    require_factory();
    if (!strcasecmp(option, "HELP")) {
      fprintf(stdout, "Options for scaler '%s':\n", _factory->description());
      _factory->describe_options(stdout, "  ");
      exit(1);

    } else {
      if (_factory->parse_option(option)) {
	mjpeg_error_exit1("Unrecognized scaler option: '%s'", option);
      }
    }

  } else {
    mjpeg_error_exit1("Unrecognized scaling parameter:  '%s'", optarg);
  }
  
}




void ysScaling::describe_parameters() const
{
  mjpeg_info("=== SCALING parameters: ================");
  if (_factory != NULL) {
    mjpeg_info("| Scaler:  %s", _factory->description());
    _factory->describe_parameters(mjpeg_info, "|          ");
  } else
    mjpeg_info("| Scaler:  *NONE*");
  if (_mono)
    mjpeg_info("| MONO:  process luma only");
  if (_line_switching)
    mjpeg_info("| LINESWITCH:  swap scanline pairs");
  if (_swap_ilace)
    mjpeg_info("| SWAP-ILACE:  drop first field, reframe stream");
}



void ysScaling::check_parameters(const ysSource &source,
				 const ysTarget &target)
{
  require_factory();

  _swap_ilace = 0;
  if (source.stream().interlace() == Y4M_ILACE_NONE) {
    if (target.stream().interlace() != Y4M_ILACE_NONE) {
      mjpeg_error_exit1("Cannot make interlaced stream from progressive.");
    }
  } else {   /* source is interlaced */
    if (target.stream().interlace() == Y4M_ILACE_NONE) {
      mjpeg_error("Cannot make progressive stream from interlaced.");
      mjpeg_error_exit1(" (Try '-I ilace=TOP_ONLY/BOTTOM_ONLY'; see manpage.)");
    } 
    if (target.stream().interlace() != source.stream().interlace()) {
      mjpeg_warn("Will swap interlacing modes (by dropping first field).");
      _swap_ilace = 1;
    }
  }

  if (_line_switching &&
      (target.stream().interlace() == Y4M_ILACE_NONE)) {
    mjpeg_warn("LINESWITCH has no effect on non-interlaced output!");
  }
}


/*
 * Handling of chroma offset:
 *
 * Proper siting of chroma subsamples requires an additional active_region
 *  offset for the chroma plane scalers.  Since this offset may be different
 *  for each plane and for each *field* (in interlaced streams), up to five
 *  different scaler setups may be required.
 *
 * The additional offset is:  qS - (qT / s),
 *  where qS is the source "effective_sample_offset()",
 *        qT is the target "effective_sample_offset()",
 *        s is the scale ratio for the axis.
 *
 * Note that if interlacing is being swapped, then qS and qT must still
 *  correspond to the actual fields, which will be different.  Hence
 *  the arguments for 'src_field' and 'tgt_field'.
 *
 */

ysRatioPoint chroma_active_offset(ysSubsampling::Plane plane,
				  ysSubsampling::Field src_field,
				  ysSubsampling::Field tgt_field,
				  const ysSource &source,
				  const ysTarget &target)
{
  DBG("ss eso (%f, %f)  ts eso (%f, %f)\n",
	  source.stream().subsampling().effective_sample_offset(src_field, plane).x().to_double(),
	  source.stream().subsampling().effective_sample_offset(src_field, plane).y().to_double(),
	  target.stream().subsampling().effective_sample_offset(src_field, plane).x().to_double(),
	  target.stream().subsampling().effective_sample_offset(src_field, plane).y().to_double());
  return
    (source.active_region().offset() 
     * source.stream().subsampling().ratio()
     / ( (src_field == ysSubsampling::FRAME) ? ysPoint(1,1) : ysPoint(1,2) ))

    + source.stream().subsampling().effective_sample_offset(src_field, plane)

    - ( target.stream().subsampling().effective_sample_offset(tgt_field, plane)
	/ ysRatioPoint(target.x_ratio(), target.y_ratio()) );
}


void ysScaling::_create_frame_scalers(const ysSource &source,
					    const ysTarget &target)
{
  if (_mono) 
    _scaler_count = 1;
  else
    _scaler_count = 3;
  _scalers = new ysScaler *[_scaler_count];
  for (int i = 0; i < _scaler_count; i++)
    _scalers[i] = _factory->new_scaler();

  _scalers[0]->setup(source.stream().framedim(0),
		     source.active_region().offset(),
		     source.matte_region(),
		     target.stream().framedim(0),
		     target.active_region(),
		     target.x_ratio(), target.y_ratio(),
		     source.bgcolor()(0));
  if (!_mono) {
    ysRatioPoint offsetCb = chroma_active_offset(ysSubsampling::PLANE_Cb,
						 ysSubsampling::FRAME,
						 ysSubsampling::FRAME,
						 source, target);
    ysRatioPoint offsetCr = chroma_active_offset(ysSubsampling::PLANE_Cr,
						 ysSubsampling::FRAME,
						 ysSubsampling::FRAME,
						 source, target);
    ysRatioPoint src_ssRatio = source.stream().subsampling().ratio();
    ysRatioPoint tgt_ssRatio = target.stream().subsampling().ratio();

    ysRatioPoint c_scale = 
      ysRatioPoint(target.x_ratio(), target.y_ratio()) *
      tgt_ssRatio / src_ssRatio;
    
    _scalers[1]->setup(source.stream().framedim(1),
		       offsetCb,
		       ysRegion(source.matte_region().dim() * src_ssRatio,
				source.matte_region().offset() * src_ssRatio),
		       target.stream().framedim(1),
		       ysRegion(target.active_region().dim() * tgt_ssRatio,
				target.active_region().offset() * tgt_ssRatio),
		       c_scale.x(), c_scale.y(), //target.x_ratio(), target.y_ratio(),
		       source.bgcolor()(1)
		       );
    _scalers[2]->setup(source.stream().framedim(2),
		       offsetCr,
		       ysRegion(source.matte_region().dim() * src_ssRatio,
				source.matte_region().offset() * src_ssRatio),
		       target.stream().framedim(2),
		       ysRegion(target.active_region().dim() * tgt_ssRatio,
				target.active_region().offset() * tgt_ssRatio),
		       c_scale.x(), c_scale.y(), //target.x_ratio(), target.y_ratio(),
		       source.bgcolor()(2)
		       );
  }
  
}

#define SC_Y        0
#define SC_UPPER_CB 1
#define SC_UPPER_CR 2
#define SC_LOWER_CB 3
#define SC_LOWER_CR 4

void ysScaling::_create_field_scalers(const ysSource &source,
				      const ysTarget &target)
{
  if (_mono)
    _scaler_count = 1;
  else
    _scaler_count = 5;
  _scalers = new ysScaler *[_scaler_count];
  for (int i = 0; i < _scaler_count; i++)
    _scalers[i] = _factory->new_scaler();


  _scalers[SC_Y]->setup(source.stream().fielddim(0),
			source.active_region().offset() / ysPoint(1,2),
			ysRegion(source.matte_region().dim() / ysPoint(1,2),
				 source.matte_region().offset() / ysPoint(1,2)),
			target.stream().fielddim(0),
			ysRegion(target.active_region().dim() / ysPoint(1,2),
				 target.active_region().offset() / ysPoint(1,2)),
			target.x_ratio(), target.y_ratio(),
			source.bgcolor()(0)
			);
  if (!_mono) {
    ysRatioPoint offsetCbUpper;
    ysRatioPoint offsetCrUpper;
    ysRatioPoint offsetCbLower;
    ysRatioPoint offsetCrLower;
    if (_swap_ilace) {
      /* if swapping ilace, then Source LOWER field --> Target UPPER field,
	                         and vice-versa.
      */
      offsetCbUpper = 
	chroma_active_offset(ysSubsampling::PLANE_Cb,
			     ysSubsampling::LOWER_FIELD,
			     ysSubsampling::UPPER_FIELD,
			     source, target);
      offsetCrUpper =
	chroma_active_offset(ysSubsampling::PLANE_Cr,
			     ysSubsampling::LOWER_FIELD,
			     ysSubsampling::UPPER_FIELD,
			     source, target);
      offsetCbLower =
	chroma_active_offset(ysSubsampling::PLANE_Cb,
			     ysSubsampling::UPPER_FIELD,
			     ysSubsampling::LOWER_FIELD,
			     source, target);
      offsetCrLower =
	chroma_active_offset(ysSubsampling::PLANE_Cr,
			     ysSubsampling::UPPER_FIELD,
			     ysSubsampling::LOWER_FIELD,
			     source, target);
    } else {
      /* if not swapping, then Source LOWER field --> Target LOWER field,
	                   etc.
      */
      offsetCbUpper = 
	chroma_active_offset(ysSubsampling::PLANE_Cb,
			     ysSubsampling::UPPER_FIELD,
			     ysSubsampling::UPPER_FIELD,
			     source, target);
      offsetCrUpper =
	chroma_active_offset(ysSubsampling::PLANE_Cr,
			     ysSubsampling::UPPER_FIELD,
			     ysSubsampling::UPPER_FIELD,
			     source, target);
      offsetCbLower =
	chroma_active_offset(ysSubsampling::PLANE_Cb,
			     ysSubsampling::LOWER_FIELD,
			     ysSubsampling::LOWER_FIELD,
			     source, target);
      offsetCrLower =
	chroma_active_offset(ysSubsampling::PLANE_Cr,
			     ysSubsampling::LOWER_FIELD,
			     ysSubsampling::LOWER_FIELD,
			     source, target);
    }
    
    ysRatioPoint src_ssRatio = source.stream().subsampling().ratio();
    ysRatioPoint tgt_ssRatio = target.stream().subsampling().ratio();

    ysRatioPoint c_scale = 
      ysRatioPoint(target.x_ratio(), target.y_ratio()) *
      tgt_ssRatio / src_ssRatio;
    
    _scalers[SC_UPPER_CB]->setup(source.stream().fielddim(1),
				 offsetCbUpper,
				 ysRegion(source.matte_region().dim() 
					  * src_ssRatio / ysPoint(1,2),
					  source.matte_region().offset()
					  * src_ssRatio / ysPoint(1,2)),
				 target.stream().fielddim(1),
				 ysRegion(target.active_region().dim()
					  * tgt_ssRatio / ysPoint(1,2),
					  target.active_region().offset()
					  * tgt_ssRatio / ysPoint(1,2)),
				 c_scale.x(), c_scale.y(),
				 source.bgcolor()(1)
				 );
    _scalers[SC_UPPER_CR]->setup(source.stream().fielddim(2),
				 offsetCrUpper,
				 ysRegion(source.matte_region().dim() 
					  * src_ssRatio / ysPoint(1,2),
					  source.matte_region().offset()
					  * src_ssRatio / ysPoint(1,2)),
				 target.stream().fielddim(2),
				 ysRegion(target.active_region().dim()
					  * tgt_ssRatio / ysPoint(1,2),
					  target.active_region().offset()
					  * tgt_ssRatio / ysPoint(1,2)),
				 c_scale.x(), c_scale.y(),
				 source.bgcolor()(2)
				 );
    _scalers[SC_LOWER_CB]->setup(source.stream().fielddim(1),
				 offsetCbLower,
				 ysRegion(source.matte_region().dim() 
					  * src_ssRatio / ysPoint(1,2),
					  source.matte_region().offset()
					  * src_ssRatio / ysPoint(1,2)),
				 target.stream().fielddim(1),
				 ysRegion(target.active_region().dim()
					  * tgt_ssRatio / ysPoint(1,2),
					  target.active_region().offset()
					  * tgt_ssRatio / ysPoint(1,2)),
				 c_scale.x(), c_scale.y(),
				 source.bgcolor()(1)
				 );
    _scalers[SC_LOWER_CR]->setup(source.stream().fielddim(2),
				 offsetCrLower,
				 ysRegion(source.matte_region().dim() 
					  * src_ssRatio / ysPoint(1,2),
					  source.matte_region().offset()
					  * src_ssRatio / ysPoint(1,2)),
				 target.stream().fielddim(2),
				 ysRegion(target.active_region().dim()
					  * tgt_ssRatio / ysPoint(1,2),
					  target.active_region().offset()
					  * tgt_ssRatio / ysPoint(1,2)),
				 c_scale.x(), c_scale.y(),
				 source.bgcolor()(2)
				 );
  }
}




/***************************************************************************/
/***************************************************************************/
/***************************************************************************/

void ysScaling::_process_frames(int fd_in, int fd_out,
				ysSource &source, ysTarget &target)
{
  int err;
  y4m_frame_info_t frameinfo;
  uint8_t *in_frame[3];
  uint8_t *out_frame[3];

  y4m_init_frame_info(&frameinfo);

  for (int i = 0; i < 3; i++) {
    in_frame[i] = new uint8_t[source.stream().framedim(i).area()];
    out_frame[i] = new uint8_t[target.stream().framedim(i).area()];
    if (_mono && (i != 0)) {
      memset(out_frame[i], 128,
	     target.stream().framedim(i).area());
    } else {
      memset(out_frame[i], target.bgcolor()(i),
	     target.stream().framedim(i).area());
    }
  }

  int frame_num = 0;
  while (1) {
    mjpeg_info ("Frame number %d", frame_num);
    err = source.read_frame(fd_in, &frameinfo, in_frame);
    if (err != Y4M_OK) goto done;

    _scalers[0]->scale(in_frame[0], out_frame[0]);
    if (!_mono) {
      _scalers[1]->scale(in_frame[1], out_frame[1]);
      _scalers[2]->scale(in_frame[2], out_frame[2]);
    }

    //err = y4m_write_frame(fd_out, target.stream().streaminfo(), &frameinfo, 
    //                      out_frame);
    err = target.write_frame(fd_out, &frameinfo, out_frame);
    if (err != Y4M_OK) goto done;
    
    frame_num++;
  }
  
 done:
  if (err == Y4M_ERR_EOF)
    mjpeg_info("End of stream at frame %d.", frame_num);
  else
    mjpeg_error_exit1("Failure at frame %d:  %s",
		      frame_num, y4m_strerr(err));
  
  for (int i = 0; i < 3; i++) {
    delete[] in_frame[i];
    delete[] out_frame[i];
  }
  y4m_fini_frame_info(&frameinfo);
}


/***************************************************************************/
/***************************************************************************/
/***************************************************************************/


void ysScaling::_process_fields(int fd_in, int fd_out,
				ysSource &source, ysTarget &target)
{
  int err;
  y4m_frame_info_t frameinfo;

  uint8_t **in_field_top;
  uint8_t **in_field_bottom;
  uint8_t **in_field_other;

  uint8_t *out_field_top[3];
  uint8_t *out_field_bottom[3];

  y4m_init_frame_info(&frameinfo);

  out_field_top[0] = new uint8_t[target.stream().fielddim(0).area()];
  out_field_top[1] = new uint8_t[target.stream().fielddim(1).area()];
  out_field_bottom[0] = new uint8_t[target.stream().fielddim(0).area()];
  memset(out_field_top[0],
	 target.bgcolor().Y, target.stream().fielddim(0).area());
  memset(out_field_bottom[0],
         target.bgcolor().Y, target.stream().fielddim(0).area());
  if (_mono) {
    out_field_top[2] = out_field_top[1];
    out_field_bottom[1] = out_field_top[1];
    out_field_bottom[2] = out_field_top[1];
    memset(out_field_top[1], 128, target.stream().fielddim(1).area());
  } else {
    out_field_top[2] = new uint8_t[target.stream().fielddim(2).area()];
    out_field_bottom[1] = new uint8_t[target.stream().fielddim(1).area()];
    out_field_bottom[2] = new uint8_t[target.stream().fielddim(2).area()];
    memset(out_field_top[1],
           target.bgcolor().Cb, target.stream().fielddim(1).area());
    memset(out_field_bottom[1],
           target.bgcolor().Cb, target.stream().fielddim(1).area());
    memset(out_field_top[2],
           target.bgcolor().Cr, target.stream().fielddim(2).area());
    memset(out_field_bottom[2],
           target.bgcolor().Cr, target.stream().fielddim(2).area());
  }

  in_field_top = new uint8_t *[3];
  in_field_bottom = new uint8_t *[3];
  in_field_other = new uint8_t *[3];

  for (int i = 0; i < 3; i++) {
    in_field_top[i] = new uint8_t[source.stream().fielddim(i).area()];
    in_field_bottom[i] = new uint8_t[source.stream().fielddim(i).area()];
    in_field_other[i] = new uint8_t[source.stream().fielddim(i).area()];
  }


  int frame_num = 0;
  while (1) {

    mjpeg_info("Frame number %d", frame_num);
    err = source.read_fields(fd_in, &frameinfo, in_field_top, in_field_bottom);
    if (err != Y4M_OK) goto done;

    if (_line_switching) {
      uint8_t **extra = in_field_top;
      in_field_top = in_field_bottom;
      in_field_bottom = extra;
    }

    if (_swap_ilace) {
      if (target.stream().interlace() == Y4M_ILACE_TOP_FIRST) {
	/* target is top-field-first */
	if (frame_num == 0) {
	  err = source.read_fields(fd_in, &frameinfo,
				   in_field_other, in_field_bottom);
	  if (err != Y4M_OK) goto done;
	}
	uint8_t **extra = in_field_top;
	in_field_top = in_field_other;
	in_field_other = extra;
      } else {
	/* target is bottom-field-first */
	if (frame_num == 0) {
	  err = source.read_fields(fd_in, &frameinfo,
				   in_field_top, in_field_other);
	  if (err != Y4M_OK) goto done;
	}
	uint8_t **extra = in_field_bottom;
	in_field_bottom = in_field_other;
	in_field_other = extra;
      }
    }

    _scalers[SC_Y]->scale(in_field_top[0], out_field_top[0]);
    _scalers[SC_Y]->scale(in_field_bottom[0], out_field_bottom[0]);
    if (!_mono) {
      _scalers[SC_UPPER_CB]->scale(in_field_top[1], out_field_top[1]);
      _scalers[SC_UPPER_CR]->scale(in_field_top[2], out_field_top[2]);
      _scalers[SC_LOWER_CB]->scale(in_field_bottom[1], out_field_bottom[1]);
      _scalers[SC_LOWER_CR]->scale(in_field_bottom[2], out_field_bottom[2]);
    }
#if 0
    err = y4m_write_fields(fd_out,
			   target.stream().streaminfo(),
			   &frameinfo, 
			   out_field_top, out_field_bottom);
#endif
    err = target.write_fields(fd_out, &frameinfo, 
			      out_field_top, out_field_bottom);
    if (err != Y4M_OK) goto done;

    frame_num++;
  }

 done:
  if (err == Y4M_ERR_EOF)
    mjpeg_info("End of stream at frame %d.", frame_num);
  else {
    mjpeg_error("Failure at frame %d:  %s", frame_num, y4m_strerr(err));
    perror("sys:");
    exit(1);
  }

  y4m_fini_frame_info(&frameinfo);
}


/***************************************************************************/
/***************************************************************************/
/***************************************************************************/



void ysScaling::create_scalers(const ysSource &source, const ysTarget &target)
{
  destroy_scalers();
  require_factory();

  if (target.stream().interlace() == Y4M_ILACE_NONE) {
    _create_frame_scalers(source, target);
  } else {
    _create_field_scalers(source, target);
  }
}



void ysScaling::destroy_scalers()
{
  if (_scalers != NULL) {
    for (int i = 0; i < _scaler_count; i++) {
      delete _scalers[i];
    }
    _scaler_count = 0;
    delete _scalers;
    _scalers = NULL;
  }
}


void ysScaling::process_stream(int fd_in, int fd_out,
			       ysSource &source, ysTarget &target)
{
  if (target.stream().interlace() != Y4M_ILACE_NONE)
    _process_fields(fd_in, fd_out, source, target);
  else
    _process_frames(fd_in, fd_out, source, target);
}

