#!/usr/bin/perl
# config-editor.pl created by James A. Pattie <james@pcxperience.com> 2003-06-11
# Copyright 2003, released under the GPL.
# Purpose: to provide a graphical editor for the ~/.ivtvrc config file used
# by the perl scripts of the ivtv project.

#
# You can always get the latest version of the script from cvs at
# http://ivtv.sourceforge.net/
#

# 20030611 -  1.0  - Initial version that builds the framework.
# 20030626 -  1.1  - Updated to cover the Audio -> AudioBitmask changes and config file versioning.
# 20030713 -  1.2  - Updated to cover the MSP Matrix options and config file version change.

use strict;

use Tk;
use Tk::Font;
use Tk::BrowseEntry;

use Getopt::Long qw(:config no_ignore_case bundling);
use Video::Frequencies 0.03;
use Config::IniFiles;

my $version="1.2";
my $cfgVersion = "2";
my $cfgVersionStr = "_configVersion_";  # hopefully unique [defaults] value to let me know what version the config file is.

my %settings = (
  ConfigFileName    => "$ENV{HOME}/.ivtvrc",
  # other settings
  Debug             => 0,
);

# map the Settings parameter to the command line variable that specifies it.
my %mappings = (
    "Debug"             => "debug",
  );

my %defaultSettings = (
  Channel           => 4,		# default to the ivtv default channel
  RecordDuration    => 3590,		# default to 59 minutes 50 seconds (in seconds) - This lets 2 back to back cron jobs work!
  InputNum          => 4,      # TV-Tuner 0
  InputName         => "Tuner 0",
  OutputDirectory   => ".",
  VideoDevice       => "/dev/video0",
  VideoWidth        => "720",	# 720x480-fullscreen NTSC
  VideoHeight       => "480",
  VideoStandard     => "NTSC",  # NTSC, PAL or SECAM
  VideoType         => "mpeg",  # mpeg, yuv
  Bitrate           => "6500000",
  PeakBitrate       => "8000000",  # peak bitrate
  Aspect            => 2,
  AudioBitmask      => 0x00e9,
  BFrames           => 3,
  DNRMode           => 0,
  DNRSpatial        => 0,
  DNRTemporal       => 0,
  DNRType           => 0,
  Framerate         => 0,
  FramesPerGOP      => 15,
  GOPClosure        => 1,
  Pulldown          => 0,
  StreamType        => 0,  # 10 = DVD format (almost)
  OutputFileName    => "video.mpg",
  FrequencyTable    => "ntsc-cable",  # default to NTSC_CABLE mapping.
  Frequency         => "", # user specified frequency.
  ResetCardSettings => 1,
  DontRecord        => 0,  # default to always recording data.
  DirectoryFormatString => "%I-%c-%d", # format string used to define the sub directory under OutputDirectory
  DateTimeFormatString  => "+%Y%m%d-%H%M", # format string used to represent the date/time if the user wants it in their DirectoryFormatString
  # msp matrix settings
  SetMSPMatrix      => 1,
  MSPInput          => 3,
  MSPOutput         => 1,
  MSPSleep          => 2,  # number of seconds the card needs before we can set the msp matrix.
  # other settings
  Debug             => 0,
  TunerNum          => 0,
);

my %configIni;      # config hash we tie to for Config::IniFiles.
my $changesUnsaved = 0; # keeps track of the current state of the config file (if it needs to be saved)

# global variables
my $profile = "";
my $attribute = "";
my $attributeValue = "";

# check for devfs support
if ( -e "/dev/.devfsd" )
{
  $defaultSettings{VideoDevice} = "/dev/v4l/video0";
}

# check for the config file
if (-f $settings{ConfigFileName})
{
  # tie to it.
  tie %configIni, 'Config::IniFiles', (-file => $settings{ConfigFileName}) or die "Error: Opening config file '$settings{ConfigFileName}' failed! $!\n";
  
  my $profile = "defaults";
  if (exists $configIni{$profile})
  {
    my $saveFile = 0;
    # check version of the config file.
    if (!exists $configIni{$profile}{$cfgVersionStr})
    {
      print "Updating config file to version 1...\n";

      # first version config file!  Update the Audio -> AudioBitmask entries.
      $configIni{$profile}{$cfgVersionStr} = $cfgVersion;

      # find all entries that have Audio and move to AudioBitmask.
      foreach my $p (keys %configIni)
      {
        if (exists $configIni{$p}{Audio})
        {
          $configIni{$p}{AudioBitmask} = $configIni{$p}{Audio};
          delete $configIni{$p}{Audio};
        }
      }

      $saveFile = 1;  # signal we need to save the config changes.
    }
    if ($configIni{$profile}{$cfgVersionStr} != $cfgVersion)
    {
      # we need to upgrade
      if ($configIni{$profile}{$cfgVersionStr} == 1)
      {
        print "Updating config file to version 2...\n";
        # add the MSP Matrix related options.
        $configIni{$profile}{SetMSPMatrix} = $settings{SetMSPMatrix};
        $configIni{$profile}{MSPInput} = $settings{MSPInput};
        $configIni{$profile}{MSPOutput} = $settings{MSPOutput};
        $configIni{$profile}{MSPSleep} = $settings{MSPSleep};
        $configIni{$profile}{$cfgVersionStr} = 2;
        $saveFile = 1;
      }
    }
    
    if ($saveFile)
    {
      # now save the updated config file before we continue.
      tied(%configIni)->RewriteConfig or die "Error: Writing config file '$settings{ConfigFileName}' failed!  $!\n";
    }
  }
  else
  {
    print "Creating [defaults] section...\n";
    # make the [defaults] section appear.
    $configIni{$profile} = {};  # make sure the section exists.

    foreach my $arg (keys %defaultSettings)
    {
      $configIni{$profile}{$arg} = $defaultSettings{$arg};
      print "configIni{$profile}{$arg} = '" . $defaultSettings{$arg} . "'\n" if $settings{Debug};
    }

    # set the config file version
    $configIni{$profile}{$cfgVersionStr} = $cfgVersion;

    # write the config file out.
    tied(%configIni)->RewriteConfig or die "Error: Writing config file '$settings{ConfigFileName}' failed!  $!\n";
  }
}
else  # create the config file
{
  print "Auto Creating config file $settings{ConfigFileName}...\n";
  my $profile = "defaults";

  # we have to create the config file and tie to it.
  tie %configIni, 'Config::IniFiles', () or die "Error: Initializing config file '$settings{ConfigFileName}' failed! $!\n";

  # now set the name to work with.
  tied(%configIni)->SetFileName($settings{ConfigFileName}) or die "Error: Setting config file to '$settings{ConfigFileName}' failed! $!\n";

  $configIni{$profile} = {};  # make sure the section exists.

  foreach my $arg (keys %defaultSettings)
  {
    $configIni{$profile}{$arg} = $defaultSettings{$arg};
    print "configIni{$profile}{$arg} = '" . $defaultSettings{$arg} . "'\n" if $settings{Debug};
  }

  # set the config file version
  $configIni{$profile}{$cfgVersionStr} = $cfgVersion;

  # write the config file out.
  tied(%configIni)->RewriteConfig or die "Error: Writing config file '$settings{ConfigFileName}' failed!  $!\n";
}

my $versionStr = "config-editor.pl $version for use with http://ivtv.sf.net/";
my $usageStr = <<"END_OF_USAGE";
$versionStr

Usage: config-editor.pl [--help] [--version] [--debug]

  -h/--help: display this help
  -v/--version: display the version of this program
  --debug: turns on debug output

Defaults:
 config file = '$settings{ConfigFileName}'

 Note:  This script relies on Perl Modules: Config::IniFiles and Getopt::Long.
END_OF_USAGE

# handle user input here
my %opts;
GetOptions(\%opts, "help|h", "version|v", "debug");
foreach my $option (keys %opts)
{
  my $found = 0;
  foreach my $mapName (keys %mappings)
  {
    if ($option =~ /^($mappings{$mapName})$/)
    {
      $settings{$mapName} = $opts{$option};
      $found = 1;
      print "$mapName = '$opts{$option}'\n" if $settings{Debug};
    }
  }
  if (!$found)
  {
    # handle the non-settings cases.
    if ($option =~ /^(v|version)$/)
    {
      print "$versionStr\n";
      exit 0;
    }
    elsif ($option =~ /^(h|help)$/)
    {
      usage(0, "");
    }
    else
    {
      usage(1, "-$option is an unknown option!");
    }
  }
}

my $mw = MainWindow->new;
$mw->title("ivtv Config Editor - $version");

my @frame;
# create any frames needed
$frame[0] = $mw->Frame( -borderwidth => 1, -relief => "ridge" )->pack( -fill => "x" );
$frame[1] = $mw->Frame( -borderwidth => 0 )->pack( -fill => "both", -expand => 1 );
$frame[2] = $frame[1]->Frame( -borderwidth => 0, -label => "Profiles" )->pack( -side => "left", -fill => "y" );
$frame[3] = $frame[1]->Frame( -borderwidth => 0, -label => "Attributes" )->pack( -side => "left", -fill => "y" );
$frame[4] = $frame[1]->Frame( -borderwidth => 0, -label => "Value" )->pack( -side => "left", -fill => "y" );
$frame[5] = $frame[2]->Frame( -borderwidth => 0, )->pack( -side => "top", -fill => "y" );
$frame[6] = $frame[2]->Frame( -borderwidth => 0, )->pack( -side => "bottom", -fill => "x" );
$frame[7] = $frame[3]->Frame( -borderwidth => 0, )->pack( -side => "top", -fill => "y" );
$frame[8] = $frame[3]->Frame( -borderwidth => 0, )->pack( -side => "bottom", -fill => "x" );
$frame[9] = $frame[1]->Frame( -borderwidth => 0, -label => "New Attributes" )->pack( -side => "left", -fill => "y" );
$frame[10] = $mw->Frame( -borderwidth => 0, -label => "Status", -labelPack => [ -side => "left" ] )->pack( -fill => "x");

# create the menu
my $menub = $frame[0]->Menubutton(-text => "File", -tearoff => 0)->pack(-side => "left", -anchor => "n");

# create the menu items.
$menub->AddItems(["command" => "Save", -command => \&saveFile, -state => "disabled"],
                 "-",
                 ["command" => "Exit", -command => \&doExit]);

# verify that the defaults profile is created.  If not, create.
my $profileName = "defaults";
if (!exists $configIni{$profileName})
{
  $configIni{$profileName} = {};  # make sure the section exists.

  foreach my $arg (keys %defaultSettings)
  {
    $configIni{$profileName}{$arg} = $defaultSettings{$arg};
    print "configIni{$profileName}{$arg} = '" . $defaultSettings{$arg} . "'\n" if $settings{Debug};
  }

  signalSaveNeeded();
}

# define the list of available profiles to work with
my $profileLB = $frame[5]->Scrolled("Listbox", -selectmode => "single", -scrollbars => "osoe", -exportselection => 0)->pack(-side => "left");
$profileLB->bind("<Button-1>", \&selectProfile);

for my $profileName (sort keys %configIni)
{
  $profileLB->insert("end", $profileName);
}
# create the profile entry field
my $valueEntry = $frame[6]->Entry(-textvariable => \$profile)->pack(-side => "top", -fill => "x");
# create the create profile button
my $createProfileB = $frame[6]->Button(-text => "Create", -command => \&createProfile)->pack(-side => "left");
# create the delete profile button
my $deleteProfileB = $frame[6]->Button(-text => "Delete", -command => \&deleteProfile, -state => "disabled")->pack(-side => "right");

# create the attributes list
my $attributeLB = $frame[7]->Scrolled("Listbox", -selectmode => "single", -scrollbars => "osoe", -exportselection => 0)->pack(-side => "left");
$attributeLB->bind("<Button-1>", \&selectAttribute);
# create the delete attribute button
my $deleteAttributeB = $frame[8]->Button(-text => "Delete", -command => \&deleteAttribute, -state => "disabled")->pack(-side => "right");

# define the value edit field
my $valueEntry = $frame[4]->Entry(-textvariable => \$attributeValue)->pack(-side => "top", -fill => "x");
# create the update attribute button
my $updateAttributeB = $frame[4]->Button(-text => "Update Attribute", -command => \&updateAttribute, -state => "disabled")->pack();

# create the new attributes list
my $newAttributeLB = $frame[9]->Scrolled("Listbox", -selectmode => "single", -scrollbars => "osoe", -exportselection => 0)->pack(-side => "top", -fill => "y");
$newAttributeLB->bind("<Button-1>", \&selectNewAttribute);

# populate the new attributes list
foreach my $arg (sort keys %defaultSettings)
{
  $newAttributeLB->insert("end", $arg);
}

# create the status text area
my $statusField = $frame[10]->Scrolled("Text", -scrollbars => "osoe", -state => "disabled", -wrap => "word", -height => "5")->pack(-side => "left", -fill => "x");
# create the tags needed for the status area
$statusField->tagConfigure("error", -foreground => "red");

MainLoop;

sub doExit
{
  if ($changesUnsaved)
  {
    # we can't exit unless they want to lose the changes
    # this code branch should not be reached due to the current implementation of Save/Exit being disabled, etc.
  }
  else
  {
    exit;
  }
}

sub saveFile
{
  # make sure the config version string exists.
  if (!exists $configIni{defaults}{$cfgVersionStr})
  {
    $configIni{defaults}{$cfgVersionStr} = $cfgVersion;
  }

  # write the config file out.
  tied(%configIni)->RewriteConfig or die "Error: Writing config file '$settings{ConfigFileName}' failed!  $!\n";

  displayStatus("Saving config file '$settings{ConfigFileName}': ok");

  $changesUnsaved = 0;

  # enable the Exit menu option
  $menub->entryconfigure(2, -state => "normal");
  # disable the Save menu option
  $menub->entryconfigure(0, -state => "disabled");
}

sub signalSaveNeeded
{
  $changesUnsaved = 1;
  # enable the Save menu option
  $menub->entryconfigure(0, -state => "normal");
  # disable the Exit menu option
  $menub->entryconfigure(2, -state => "disabled");
}

sub selectProfile
{
  my $sel = $profileLB->curselection();
  if ($sel ne "")
  {
    $profile = $profileLB->get($sel);
    displayStatus("Selected Profile: " . $profile);

    # now populate the attributes list box after first clearing it of any old values.
    $attributeLB->delete(0, 'end');
    foreach my $attribute (sort keys %{$configIni{$profile}})
    {
      if ($attribute ne $cfgVersionStr)  # don't show the user the config version string.
      {
        $attributeLB->insert("end", $attribute);
      }
    }

    # enable the delete profile button
    $deleteProfileB->configure(-state => "normal");

    # disable the update attribute button
    $updateAttributeB->configure(-state => "disabled");

    # reset the attribute value field to nothing.
    $attributeValue = "";
  }
}

sub createProfile
{
  if ($profile)
  {
    if (!exists $configIni{$profile})
    {
      displayStatus("Creating Profile [$profile]...");
      $configIni{$profile} = {};

      # now display the profiles available.
      $profileLB->delete(0, 'end');
      for my $profileName (sort keys %configIni)
      {
        $profileLB->insert("end", $profileName);
      }
      
      signalSaveNeeded();
    }
    else
    {
      displayStatus("Profile [$profile] already exists.  Not creating.");
    }
  }
  else
  {
    displayError("You must specify a profile to create!");
  }
}

sub deleteProfile
{
  if ($profile)
  {
    if ($profile ne "defaults")
    {
      displayStatus("Deleting Profile [$profile]");

      signalSaveNeeded();

      delete $configIni{$profile};
      $profile = "";

      # remove all the profile entries and re-populate
      $profileLB->delete(0, "end");
      for my $profile (sort keys %configIni)
      {
        $profileLB->insert("end", $profile);
      }

      # remove all attribute entries
      $attributeLB->delete(0, 'end');

      # make sure attribute value is empty
      $attributeValue = "";

      # disable the update attribute button
      $updateAttributeB->configure(-state => "disabled");

      # disable the delete profile button
      $deleteProfileB->configure(-state => "disabled");

      # disable the delete attribute button
      $deleteAttributeB->configure(-state => "disabled");
    }
    else
    {
      displayError("You can not delete the [defaults] profile!");
    }
  }
  else
  {
    displayError("You must select a Profile to delete!");
  }
}

sub selectAttribute
{
  my $sel = $attributeLB->curselection();
  if ($sel ne "")
  {
    $attribute = $attributeLB->get($sel);
    $attributeValue = $configIni{$profile}{$attribute};
    displayStatus("Selected Attribute: $attribute, value = '$attributeValue'");

    # enable the update attribute button
    $updateAttributeB->configure(-state => "normal");

    # enable the delete attribute button
    $deleteAttributeB->configure(-state => "normal");
  }
}

sub selectNewAttribute
{
  my $sel = $newAttributeLB->curselection();
  if ($sel ne "")
  {
    $attribute = $newAttributeLB->get($sel);
    if (!$profile)
    {
      displayError("You must select a profile first!");
      return;
    }
    if (!exists $configIni{$profile}{$attribute} || $configIni{$profile}{$attribute} ne $defaultSettings{$attribute})
    {
      $attributeValue = $defaultSettings{$attribute};
      $configIni{$profile}{$attribute} = $attributeValue;
      signalSaveNeeded();

      displayStatus("Created Attribute: $attribute, value = '$attributeValue'");

      # remove all attribute entries and re-populate
      $attributeLB->delete(0, 'end');
      foreach my $attribute (sort keys %{$configIni{$profile}})
      {
        $attributeLB->insert("end", $attribute);
      }

      # enable the update attribute button
      $updateAttributeB->configure(-state => "normal");

      # enable the delete attribute button
      $deleteAttributeB->configure(-state => "normal");
      
      # unselect the New Attribute selection
      $newAttributeLB->selectionClear($sel);
    }
  }
}

sub deleteAttribute
{
  delete $configIni{$profile}{$attribute};

  # remove all attribute entries and re-populate
  $attributeLB->delete(0, 'end');
  foreach my $attribute (sort keys %{$configIni{$profile}})
  {
    $attributeLB->insert("end", $attribute);
  }

  # make sure attribute value is empty
  $attributeValue = "";

  # disable the update attribute button
  $updateAttributeB->configure(-state => "disabled");

  # disable the delete attribute button
  $deleteAttributeB->configure(-state => "disabled");
}

sub updateAttribute
{
  if ($configIni{$profile}{$attribute} ne $attributeValue)
  {
    signalSaveNeeded();

    # I really should do some validation...
    $configIni{$profile}{$attribute} = $attributeValue;

    # disable the update attribute button
    $updateAttributeB->configure(-state => "disabled");
  }
}

sub displayStatus
{
  my $output = shift;

  # enable writing to the status box
  $statusField->configure(-state => "normal");

  $statusField->insert('end lineend', $output . "\n");
  
  # disable writing to the status box
  $statusField->configure(-state => "disabled");

  $statusField->see('end');
}

sub displayError
{
  my $output = shift;

  # enable writing to the status box
  $statusField->configure(-state => "normal");

  $statusField->insert('end', "Error", "error");

  # disable writing to the status box
  $statusField->configure(-state => "disabled");

  displayStatus(": $output");
}

# usage(returnValue, ErrorString)
# returnValue = 1 or 0
# ErrorString = message you want to tell the user, but only if returnValue = 1.
sub usage
{
  my $errorCode = shift;
  my $error = shift;

  print $usageStr;
  print "\nError:  $error\n" if ($errorCode == 1);
  print "$error\n" if ($errorCode == 2);

  exit $errorCode;
}
