//----------------------------------------------------------------------------
//
// File:        disk.cpp
// Date:        23-Feb-1998
// Programmer:  Marc Rousseau
//
// Description: A simple program to display the contents of a TI-Disk image
//              created by AnaDisk.  It also supports extracting files from
//              the disk and optionally converting them to 'standard' PC type
//              files.
//
// Copyright (c) 1998-2004 Marc Rousseau, All Rights Reserved.
//
// This program 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.
//
// This program 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 this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307, USA.
//
// Revision History:
//
//   15-Jan-2003    Created classes to encapsulate TI file & disks
//
//----------------------------------------------------------------------------

#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include "common.hpp"
#include "logger.hpp"
#include "disk-media.hpp"
#include "file-system.hpp"
#include "fileio.hpp"
#include "option.hpp"
#include "support.hpp"

#if defined ( OS_OS2 )
	#include <conio.h>
#elif defined ( OS_WINDOWS )
	#include <conio.h>
#elif defined ( OS_LINUX )
	#include <unistd.h>
#endif

DBG_REGISTER( __FILE__ );

void ConvertFilename( char *buffer )
{
	FUNCTION_ENTRY( nullptr, "ConvertFilename", true );

	char *end = buffer;
	while( *end )
	{
		end++;
	}
	while( *--end == ' ' )
	{
	}
	*++end = '\0';
	if( *buffer == '-' )
	{
		*buffer = '_';
	}
	for(; *buffer; buffer++ )
	{
		if( isalnum( *buffer ))
		{
			continue;
		}
		if( *buffer == '_' )
		{
			continue;
		}
		if( *buffer == '-' )
		{
			continue;
		}
		*buffer = '_';
	}
}

static inline UINT16 GetUINT16( const void *_ptr )
{
	FUNCTION_ENTRY( nullptr, "GetUINT16", true );

	const UINT8 *ptr = ( const UINT8 * ) _ptr;
	return ( UINT16 ) (( ptr[ 0 ] << 8 ) | ptr[ 1 ] );
}

void DumpFile( cFile *file, bool convertFiles )
{
	FUNCTION_ENTRY( nullptr, "DumpFile", true );

	const sFileDescriptorRecord *fdr = file->GetFDR( );

	if( cFileSystem::IsValidFDR( fdr ) == false )
	{
		return;
	}

	char name[ 11 ];
	strncpy( name, fdr->FileName, 10 );
	name[ 10 ] = '\0';
	ConvertFilename( name );

	// Decorate the name to match the file's type
	if( convertFiles == true )
	{
		if( file->IsProgram( ))
		{
			strcat( name, ".PROG" );
		}
		else
		{
			sprintf( name + strlen( name ), ".%c%c%d",
			         file->IsInternal( ) ? 'I' : 'D',
			         file->IsVariable( ) ? 'V' : 'F',
			         file->RecordLength( ));
		}
	}

	// Find a unique filename to use
	char filename[ 80 ];
	sprintf( filename, "%s", name );
	for( int i = 0; i < 255; i++ )
	{
		FILE *testFile = fopen( filename, "rb" );
		if( testFile == nullptr )
		{
			break;
		}
		fclose( testFile );
		sprintf( filename, "%s.%03d", name, i );
	}

	const char *mode = ( convertFiles && file->IsDisplay( )) ? "wt" : "wb";

	// Open the file
	FILE *outFile = fopen( filename, mode );
	if( outFile == nullptr )
	{
		return;
	}

	// Write the file header (v9t9 FIAD)
	if( convertFiles == false )
	{
		char header[ 128 ];
		memset( header, 0, 128 );
		memcpy( header, fdr, 20 );
		fwrite( header, 128, 1, outFile );
	}

	if( convertFiles == true )
	{
		bool errors = false;

		int count;
		do
		{
			char sector[ DEFAULT_SECTOR_SIZE ];
			count = file->ReadRecord( sector, DEFAULT_SECTOR_SIZE );
			if( count >= 0 )
			{
				// Handle variable length, internal type files
				if( file->IsVariable( ) && file->IsInternal( ))
				{
					fputc( count, outFile );
				}
				fwrite( sector, count, 1, outFile );
				// Handle display type files
				if( file->IsDisplay( ))
				{
					for( int i = 0; i < count; i++ )
					{
						errors |= ( sector[ i ] == '\n' );
					}
					fprintf( outFile, "\n" );
				}
			}
		}
		while( count >= 0 );

		if( errors )
		{
			fprintf( stderr, "File '%s' contains newline characters and will not recoverable\n", name );
		}
	}
	else
	{
		int totalSectors = file->TotalSectors( );
		for( int i = 0; i < totalSectors; i++ )
		{
			char sector[ DEFAULT_SECTOR_SIZE ];
			if( file->ReadSector( i, sector ) < 0 )
			{
				fprintf( stderr, "I/O Error reading file %10.19s\n", fdr->FileName );
				break;
			}
			fwrite( sector, DEFAULT_SECTOR_SIZE, 1, outFile );
		}
	}

	fclose( outFile );
}

void PrintDiskLayout( cDiskMedia *disk )
{
	FUNCTION_ENTRY( nullptr, "PrintDiskLayout", true );

	track::Format format     = track::Format::Unknown;
	track::Format lastFormat = track::Format::Unknown;

	printf( "\n" );

	for( int h = 0; h < disk->NumSides( ); h++ )
	{
		for( int t = 0; t < disk->NumTracks( ); t++ )
		{
			iDiskTrack *track = disk->GetTrack( t, h );
			if( track == nullptr )
			{
				continue;
			}
			if( format == track::Format::Unknown )
			{
				format = track->GetFormat( );
			}
			printf( "Track: %2d  Side: %d - ", t, h );
			if( track->GetFormat( ) == track::Format::Unknown )
			{
				printf( "is not formatted\n" );
				continue;
			}
			if( lastFormat != track->GetFormat( ))
			{
				if( lastFormat != track::Format::Unknown )
				{
					printf( "format changed -" );
				}
				lastFormat = track->GetFormat( );
			}
			int noSectors = 0;
			for( auto &sector : track->GetSectors( ))
			{
				printf( " %d", sector->LogicalSector( ));
				noSectors++;
			}
			printf( "\n" );
			for( auto &sector : track->GetSectors( ))
			{
				if( sector->GetData( ) == nullptr )
				{
					printf( "  sector %d does not contain any data\n", sector->LogicalSector( ));
				}
				if( sector->DataMark( ) != MARK_DAM )
				{
					printf( "  contains deleted sector %d\n", sector->LogicalSector( ));
				}
				if( sector->LogicalCylinder( ) != t )
				{
					printf( "  sector %d has incorrect cylinder %d\n", sector->LogicalSector( ), sector->LogicalCylinder( ));
				}
				if( sector->LogicalHead( ) != h )
				{
					printf( "  sector %d has incorrect side %d\n", sector->LogicalSector( ), sector->LogicalHead( ));
				}
				if( sector->LogicalSize( ) != 1 )
				{
					printf( "  sector %d size is %d bytes\n", sector->LogicalSector( ), ( int ) sector->Size( ));
				}
			}
		}
	}
}

void PrintDiskInfo( cDiskMedia *disk )
{
	FUNCTION_ENTRY( nullptr, "PrintDiskInfo", true );

	printf( "\n" );

	const char *strFormat = "<Unknown>";
	switch( disk->GetFormat( ))
	{
		case FORMAT_RAW_TRACK :
			strFormat = "PC99";
			break;
		case FORMAT_RAW_SECTOR :
			strFormat = "v9t9";
			break;
		case FORMAT_ANADISK :
			strFormat = "AnaDisk";
			break;
		case FORMAT_CF7 :
			strFormat = "CF7+";
			break;
		case FORMAT_HFE :
			strFormat = "HFE";
			break;
		default :
			break;
	}

	bool mixed = false;
	track::Format format = track::Format::Unknown;

	for( int h = 0; h < disk->NumSides( ); h++ )
	{
		for( int t = 0; t < disk->NumTracks( ); t++ )
		{
			iDiskTrack *track = disk->GetTrack( t, h );
			if( track == nullptr )
			{
				continue;
			}
			if( format == track::Format::Unknown )
			{
				format = track->GetFormat( );
			}
			else if(( format != track->GetFormat( )) && ( track->GetFormat( ) != track::Format::Unknown ))
			{
				mixed = true;
			}
		}
	}

	const char *txtDensity = "<Unknown>";

	switch( format )
	{
		case track::Format::Unknown :
			txtDensity = "<Unknown>";
			break;
		case track::Format::FM :
			txtDensity = "Single";
			break;
		case track::Format::MFM :
			txtDensity = "Double";
			break;
	}

	printf( "     File: %s", disk->GetName( ));
	if( disk->GetFormat( ) == FORMAT_CF7 )
	{
		printf( " - Volume %d", disk->GetVolume( ));
	}
	printf( "\n" );
	printf( "   Format: %s\n", strFormat );
	printf( "   Tracks: %d\n", disk->NumTracks( ));
	printf( "    Sides: %d\n", disk->NumSides( ));
	printf( "  Density: %s\n", mixed ? "Mixed" : txtDensity );
}

bool ParseFileName( const char *arg, void *base )
{
	FUNCTION_ENTRY( nullptr, "ParseFileName", true );

	const char *ptr = strchr( arg, '=' );

	// Handle special case of extract all
	if( ptr == nullptr )
	{
		fprintf( stderr, "A filename needs to be specified: '%s'\n", arg );
		return false;
	}

	std::string *name = ( std::string * ) base;

	*name = ptr + 1;

	return true;
}

bool ParseFileNames( const char *arg, void *base )
{
	FUNCTION_ENTRY( nullptr, "ParseFileNames", true );

	const char *ptr = strchr( arg, '=' );

	// Handle special case of extract all
	if( ptr == nullptr )
	{
		fprintf( stderr, "A filename needs to be specified: '%s'\n", arg );
		return false;
	}

	std::list<std::string> *list = ( std::list<std::string> * ) base;

	list->push_back( ptr + 1 );

	return true;
}

void PrintUsage( )
{
	FUNCTION_ENTRY( nullptr, "PrintUsage", true );

	fprintf( stdout, "Usage: disk [options] file\n" );
	fprintf( stdout, "\n" );
}

bool HasTimestamp( const sFileDescriptorRecord *fdr )
{
	for( int j = 0; j < 8; j++ )
	{
		if( fdr->reserved2[ j ] != 0 )
		{
			return true;
		}
	}

	return false;
}

void ListFiles( FILE *outFile, cRefPtr<cFileSystem> disk )
{
	std::list<std::string> names;

	disk->GetFilenames( names );

	for( auto &name : names )
	{
		fprintf( outFile, "%s\n", name.c_str( ));
	}
}

void ShowDirectory( FILE *outFile, cRefPtr<cFileSystem> disk, bool verboseMode, bool showSha1 )
{
	bool bFoundTimestamp = false;

	// Get a list of all the files and directories
	struct sDirectory
	{
		int                       index;
		std::string               name;
		std::list<cRefPtr<cFile>> files;
	};

	std::list<sDirectory> directories;
	for( int i = -1; i < disk->DirectoryCount( ); i++ )
	{
		const char *dirName = disk->DirectoryName( i );

		sDirectory dir;

		dir.index = i;
		dir.name = dirName ? std::string( "." ) + dirName : "";

		std::list<std::string> filenames;
		disk->GetFilenames( filenames, i );

		for( auto &name : filenames )
		{
			cRefPtr<cFile> file = disk->OpenFile( name.c_str( ), i );
			if( file != nullptr )
			{
				dir.files.push_back( file );
				bFoundTimestamp |= HasTimestamp( file->GetFDR( ));
			}
		}

		directories.push_back( dir );
	}

	std::string name;
	disk->GetName( name );

	int sectorsUsed = 0;
	int auSize = disk->AllocationSize( );

	int flags = 0;
	flags |= bFoundTimestamp ? LISTING_FLAG_TIMESTAMPS : 0;
	flags |= verboseMode ? LISTING_FLAG_VERBOSE : 0;
	flags |= showSha1 ? LISTING_FLAG_SHA1 : 0;

	for( auto &dir : directories )
	{
		fprintf( outFile, "\n" );
		fprintf( outFile, "Directory of %s%s\n", name.c_str( ), dir.name.c_str( ));
		fprintf( outFile, "\n" );

		std::list<std::string> headers;
		disk->ListingHeader( flags, headers );

		for( auto &header : headers )
		{
			fprintf( outFile, " %s", header.c_str( ));
		}
		fprintf( outFile, "\n" );

		for( auto &header : headers )
		{
			int length = header.length( );
			fprintf( outFile, " %*.*s", length, length, "========================================" );
		}
		fprintf( outFile, "\n" );

		for( auto &file : dir.files )
		{
			const sFileDescriptorRecord *fdr = file->GetFDR( );

			int size = (( GetUINT16( &fdr->TotalSectors ) + auSize / 2 ) / auSize + 1 ) * auSize;
			if( size <= disk->TotalSectors( ))
			{
				sectorsUsed += size;
			}

			std::list<std::string> fields;
			disk->ListingData( file, dir.index, flags, fields );

			for( auto &field : fields )
			{
				fprintf( outFile, " %s", field.c_str( ));
			}

			fprintf( outFile, "\n" );
		}
	}

	int totalSectors   = disk->TotalSectors( );
	int totalAvailable = disk->FreeSectors( );

	fprintf( outFile, "\n" );
	fprintf( outFile, "  Available: %4d  Used: %4d\n", totalAvailable, sectorsUsed );
	fprintf( outFile, "      Total: %4d   Bad: %4d\n", totalSectors, totalSectors - sectorsUsed - totalAvailable );
	fprintf( outFile, "\n" );
}

int main( int argc, char *argv[] )
{
	FUNCTION_ENTRY( nullptr, "main", true );

	bool dumpFiles    = false;
	bool checkDisk    = false;
	bool bareFiles    = false;
	bool convertFiles = false;
	bool verboseMode  = false;
	bool showSha1     = false;
	bool showLayout   = false;

	std::list<std::string> addFiles;
	std::list<std::string> delFiles;
	std::list<std::string> extFiles;

	std::string outputFilename;

	eDiskFormat outputFormat = FORMAT_UNKNOWN;

	sOption optList[ ] =
	{
		{ 'a', "add=*<filename>",      OPT_NONE,                      true,              &addFiles,       ParseFileNames, "Add <filename> to the disk image" },
		{  0,  "check",                OPT_VALUE_SET | OPT_SIZE_BOOL, true,              &checkDisk,      nullptr,        "Check the integrity of the disk structures" },
		{ 'b', "bare",                 OPT_VALUE_SET | OPT_SIZE_BOOL, true,              &bareFiles,      nullptr,        "Restrict output to a list of files" },
		{ 'c', "convert",              OPT_VALUE_SET | OPT_SIZE_BOOL, true,              &convertFiles,   nullptr,        "Convert extracted files to DOS files" },
		{ 'd', "dump",                 OPT_VALUE_SET | OPT_SIZE_BOOL, true,              &dumpFiles,      nullptr,        "Extract all files to FIAD files" },
		{ 'l', "layout",               OPT_VALUE_SET | OPT_SIZE_BOOL, true,              &showLayout,     nullptr,        "Display the disk sector layout" },
		{  0,  "output=pc99",          OPT_VALUE_SET | OPT_SIZE_INT,  FORMAT_RAW_TRACK,  &outputFormat,   nullptr,        "Convert disk to PC99 format" },
		{  0,  "output=v9t9",          OPT_VALUE_SET | OPT_SIZE_INT,  FORMAT_RAW_SECTOR, &outputFormat,   nullptr,        "Convert disk to v9t9 DOAD format" },
		{  0,  "output=anadisk",       OPT_VALUE_SET | OPT_SIZE_INT,  FORMAT_ANADISK,    &outputFormat,   nullptr,        "Convert disk to AnaDisk format /w headers" },
		{  0,  "output=cf7+",          OPT_VALUE_SET | OPT_SIZE_INT,  FORMAT_CF7,        &outputFormat,   nullptr,        "Convert disk to CF7+ format" },
		{  0,  "filename=*<filename>", OPT_NONE,                      true,              &outputFilename, ParseFileName,  "Name of the file to convert to" },
		{ 'r', "remove=*<filename>",   OPT_NONE,                      true,              &delFiles,       ParseFileNames, "Remove <filename> from the disk image" },
		{ 'v', "verbose",              OPT_VALUE_SET | OPT_SIZE_BOOL, true,              &verboseMode,    nullptr,        "Display information about the disk image" },
		{ 's', "sha1",                 OPT_VALUE_SET | OPT_SIZE_BOOL, true,              &showSha1,       nullptr,        "Display the SHA1 checksum for each file" },
		{ 'e', "extract*=<filename>",  OPT_NONE,                      true,              &extFiles,       ParseFileNames, "Extract <filename> to v9t9 FIAD file" }
	};

	/*
	   -f, --fix

	   FDR >0014->001B    >8F >1B >09 >02 >8F >27 >09 >02
	 */

	if( argc == 1 )
	{
		PrintHelp( SIZE( optList ), optList );
		return 0;
	}

	#if defined ( OS_LINUX )
		if( isatty( fileno( stdout )))
		{
			printf( "TI-99/4A Diskette Viewer\n" );
		}
	#endif

	cRefPtr<cFileSystem> disk;

	int index = 1;
	while( index < argc )
	{
		index = ParseArgs( index, argc, argv, SIZE( optList ), optList );

		if( index < argc )
		{
			if( disk != nullptr )
			{
				fprintf( stderr, "Only one disk image file can be specified\n" );
				return -1;
			}
			disk = cFileSystem::Open( argv[ index++ ] );
			if( disk == nullptr )
			{
				fprintf( stderr, "Unable to open disk image file \"%s\"\n", argv[ index-1 ] );
				return -1;
			}
		}
	}

	if( disk == nullptr )
	{
		fprintf( stderr, "No disk image file specified\n" );
		return -1;
	}

	if( disk->IsValid( ) == false )
	{
		std::string name;
		disk->GetPath( name );
		fprintf( stderr, "File \"%s\" does not contain a recognized disk format\n", name.c_str( ));
		return -1;
	}

	cRefPtr<cDiskMedia> media;

	cDiskFileSystem* fs = dynamic_cast<cDiskFileSystem*> ( disk.get( ));
	if( fs != nullptr )
	{
		media = fs->GetMedia( );

		if(( media->GetVolume( ) == 0 ) && ( media->MaxVolume( ) > 0 ))
		{
			std::string filename;
			disk->GetPath( filename );
			for( int i = 1; i <= media->MaxVolume( ); i++ )
			{
				char volumeName[ 4096 ];
				sprintf( volumeName, "%s#%d", filename.c_str( ), i );
				cRefPtr<cFileSystem> vol = cFileSystem::Open( volumeName );
				if(( vol != nullptr ) && vol->IsValid( ))
				{
					if( verboseMode == true )
					{
						PrintDiskInfo( dynamic_cast<cDiskFileSystem*> ( vol.get( ))->GetMedia( ));
					}

					ShowDirectory( stdout, vol, verboseMode, showSha1 );

					if( checkDisk )
					{
						vol->CheckDisk( verboseMode );
					}
				}
			}
			return 0;
		}

		if( media->GetFormat( ) != FORMAT_UNKNOWN )
		{
			if( showLayout == true )
			{
				PrintDiskLayout( media );
			}
			if( verboseMode == true )
			{
				PrintDiskInfo( media );
			}
		}
	}

	if( dumpFiles == true )
	{
		// Ignore any files specified with '--extract=' and dump them all
		disk->GetFilenames( extFiles );
	}

	// Extract existing files
	if( extFiles.empty( ) == false )
	{
		if( verboseMode == true )
		{
			fprintf( stdout, "\n" );
		}
		if( verboseMode == true )
		{
			fprintf( stdout, "Extracting files:\n" );
		}
		for( auto extFile : extFiles )
		{
			cRefPtr<cFile> file = disk->OpenFile( extFile.c_str( ));
			if( file != nullptr )
			{
				if( verboseMode == true )
				{
					fprintf( stdout, "  %s\n", extFile.c_str( ));
				}
				DumpFile( file, convertFiles );
			}
			else
			{
				fprintf( stderr, "  Unable to locate file %s\n", extFile.c_str( ));
			}
		}
	}

	// Remove existing files
	if( delFiles.empty( ) == false )
	{
		if( verboseMode == true )
		{
			fprintf( stdout, "\n" );
		}
		if( verboseMode == true )
		{
			fprintf( stdout, "Removing files:\n" );
		}
		for( auto delFile : delFiles )
		{
			if( disk->DeleteFile( delFile.c_str( )) == true )
			{
				if( verboseMode == true )
				{
					fprintf( stdout, "  %s\n", delFile.c_str( ));
				}
			}
			else
			{
				fprintf( stderr, "  Unable to delete file %s\n", delFile.c_str( ));
			}
		}
	}

	// Add new files here
	if( addFiles.empty( ) == false )
	{
		if( verboseMode == true )
		{
			fprintf( stdout, "\n" );
		}
		if( verboseMode == true )
		{
			fprintf( stdout, "Adding files:\n" );
		}
		for( auto addFile : addFiles )
		{
			cRefPtr<cFile> file = cFile::Open( addFile.c_str( ));
			if( file != nullptr )
			{
				if( verboseMode == true )
				{
					fprintf( stdout, "  %s\n", addFile.c_str( ));
				}
				if( disk->AddFile( file ) == false )
				{
					fprintf( stderr, "  Unable to add file %s\n", addFile.c_str( ));
				}
			}
			else
			{
				fprintf( stderr, "  Unable to locate file %s\n", addFile.c_str( ));
			}
		}
	}

	if( bareFiles )
	{
		ListFiles( stdout, disk );
	}
	else
	{
		ShowDirectory( stdout, disk, verboseMode, showSha1 );

		if( checkDisk )
		{
			disk->CheckDisk( verboseMode );
		}
	}

	if( media != nullptr )
	{
		// First, write out any changes made
		if( media->HasChanged( ) == true )
		{
			if( media->SaveFile( ) == false )
			{
				fprintf( stderr, "Unable to write to file \"%s\"\n", media->GetName( ));
				return -1;
			}
		}

		// See if a conversion was requested
		if( outputFormat != FORMAT_UNKNOWN )
		{
			if( outputFilename.empty( ))
			{
				outputFilename = media->GetName( );
			}

			if(( outputFormat == media->GetFormat( )) && ( outputFilename == media->GetName( )))
			{
				fprintf( stderr, "Conversion skipped - output file matches source\n" );
				return 0;
			}

			// Make sure we don't wipe out other volumes on a CF7+ disk by changing formats
			if(( media->GetFormat( ) == FORMAT_CF7 ) && ( outputFormat != FORMAT_CF7 ) && ( outputFilename == media->GetName( )))
			{
				fprintf( stderr, "Changing a CF7+ disk's format is not supported\n" );
				return -1;
			}

			if( media->SaveFileAs( outputFilename.c_str( ), outputFormat ) == false )
			{
				fprintf( stderr, "Unable to write to file \"%s\"\n", outputFilename.c_str( ));
				return -1;
			}
		}
	}

	return 0;
}
