/***************************************************************************
                          skyobject.cpp  -  Trinity Desktop Planetarium
                             -------------------
    begin                : Sun Feb 11 2001
    copyright            : (C) 2001 by Jason Harris
    email                : jharris@30doradus.org
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/

#include <iostream>

#include <tdeglobal.h>
#include <kstandarddirs.h>
#include <tqpoint.h>
#include <tqregexp.h>
#include <tqfile.h>
#include <tqtextstream.h>

#include "skyobject.h"
#include "starobject.h" //needed in saveUserLog()
#include "ksnumbers.h"
#include "dms.h"
#include "geolocation.h"
#include "kstarsdatetime.h"

TQString SkyObject::emptyString = TQString("");
TQString SkyObject::unnamedString = TQString(i18n("unnamed"));
TQString SkyObject::unnamedObjectString = TQString(i18n("unnamed object"));
TQString SkyObject::starString = TQString("star");

SkyObject::SkyObject( SkyObject &o ) : SkyPoint( o ) {
	setType( o.type() );
	Magnitude = o.mag();
	setName(o.name());
	setName2(o.name2());
	setLongName(o.longname());
	ImageList = o.ImageList;
	ImageTitle = o.ImageTitle;
	InfoList = o.InfoList;
	InfoTitle = o.InfoTitle;
}

SkyObject::SkyObject( int t, dms r, dms d, float m,
						TQString n, TQString n2, TQString lname ) : SkyPoint( r, d) {
	setType( t );
	Magnitude = m;
	Name = 0;
	setName(n);
	Name2 = 0;
	setName2(n2);
	LongName = 0;
	setLongName(lname);
}

SkyObject::SkyObject( int t, double r, double d, float m,
						TQString n, TQString n2, TQString lname ) : SkyPoint( r, d) {
	setType( t );
	Magnitude = m;
	Name = 0;
	setName(n);
	Name2 = 0;
	setName2(n2);
	LongName = 0;
	setLongName(lname);
}

SkyObject::~SkyObject() {
	delete Name;
	delete Name2;
	delete LongName;
}

void SkyObject::setLongName( const TQString &longname ) {
	delete LongName;
	if ( longname.isEmpty() ) {
		if ( hasName() )
			LongName = new TQString(translatedName());
		else if ( hasName2() )
			LongName = new TQString(*Name2);
		else
			LongName = 0;
	} else {
		LongName = new TQString(longname);
	}
}

TQTime SkyObject::riseSetTime( const KStarsDateTime &dt, const GeoLocation *geo, bool rst ) {
	//this object does not rise or set; return an invalid time
	if ( checkCircumpolar(geo->lat()) )
		return TQTime( 25, 0, 0 );

	//First of all, if the object is below the horizon at date/time dt, adjust the time 
	//to bring it above the horizon
	KStarsDateTime dt2 = dt;
	SkyPoint p = recomputeCoords( dt, geo );
	dms gegsls = (geo->GSTtoLST( dt.gst() ));
	p.EquatorialToHorizontal( &gegsls, geo->lat() );
	if ( p.alt()->Degrees() < 0.0 ) {
		if ( p.az()->Degrees() < 180.0 ) { //object has not risen yet
			dt2 = dt.addSecs( 12.*3600. );
		} else { //object has already set
			dt2 = dt.addSecs( -12.*3600. );
		}
	}
	
	return geo->UTtoLT( KStarsDateTime( dt2.date(), riseSetTimeUT( dt2, geo, rst ) ) ).time();
}

TQTime SkyObject::riseSetTimeUT( const KStarsDateTime &dt, const GeoLocation *geo, bool riseT ) {
	// First trial to calculate UT
	TQTime UT = auxRiseSetTimeUT( dt, geo, ra(), dec(), riseT );
	
	// We iterate once more using the calculated UT to compute again
	// the ra and dec for that time and hence the rise/set time.
	// Also, adjust the date by +/- 1 day, if necessary
	KStarsDateTime dt0 = dt;
	dt0.setTime( UT );
	if ( riseT && dt0 > dt ) {
		dt0 = dt0.addDays( -1 );
	} else if ( ! riseT && dt0 < dt ) {
		dt0 = dt0.addDays( 1 );
	}
	
	SkyPoint sp = recomputeCoords( dt0, geo );
	UT = auxRiseSetTimeUT( dt0, geo, sp.ra(), sp.dec(), riseT );

	// We iterate a second time (For the Moon the second iteration changes
	// aprox. 1.5 arcmin the coordinates).
	dt0.setTime( UT );
	sp = recomputeCoords( dt0, geo );
	UT = auxRiseSetTimeUT( dt0, geo, sp.ra(), sp.dec(), riseT );
	return UT;
}

dms SkyObject::riseSetTimeLST( const KStarsDateTime &dt, const GeoLocation *geo, bool riseT ) {
	KStarsDateTime rst( dt.date(), riseSetTimeUT( dt, geo, riseT) );
	return geo->GSTtoLST( rst.gst() );
}

TQTime SkyObject::auxRiseSetTimeUT( const KStarsDateTime &dt, const GeoLocation *geo,
			const dms *righta, const dms *decl, bool riseT) {
	dms LST = auxRiseSetTimeLST( geo->lat(), righta, decl, riseT );
	return dt.GSTtoUT( geo->LSTtoGST( LST ) );
}

dms SkyObject::auxRiseSetTimeLST( const dms *gLat, const dms *righta, const dms *decl, bool riseT ) {
	dms h0 = elevationCorrection();
	double H = approxHourAngle ( &h0, gLat, decl );
	dms LST;

	if ( riseT )
		LST.setH( 24.0 + righta->Hours() - H/15.0 );
	else
		LST.setH( righta->Hours() + H/15.0 );

	return LST.reduce();
}


dms SkyObject::riseSetTimeAz( const KStarsDateTime &dt, const GeoLocation *geo, bool riseT ) {
	dms Azimuth;
	double AltRad, AzRad;
	double sindec, cosdec, sinlat, coslat, sinHA, cosHA;
	double sinAlt, cosAlt;

	TQTime UT = riseSetTimeUT( dt, geo, riseT );
	KStarsDateTime dt0 = dt;
	dt0.setTime( UT );
	SkyPoint sp = recomputeCoords( dt0, geo );
	const dms *ram = sp.ra0();
	const dms *decm = sp.dec0();

	dms LST = auxRiseSetTimeLST( geo->lat(), ram, decm, riseT );
	dms HourAngle = dms( LST.Degrees() - ram->Degrees() );

	geo->lat()->SinCos( sinlat, coslat );
	dec()->SinCos( sindec, cosdec );
	HourAngle.SinCos( sinHA, cosHA );

	sinAlt = sindec*sinlat + cosdec*coslat*cosHA;
	AltRad = asin( sinAlt );
	cosAlt = cos( AltRad );

	AzRad = acos( ( sindec - sinlat*sinAlt )/( coslat*cosAlt ) );
	if ( sinHA > 0.0 ) AzRad = 2.0*dms::PI - AzRad; // resolve acos() ambiguity
	Azimuth.setRadians( AzRad );

	return Azimuth;
}

TQTime SkyObject::transitTimeUT( const KStarsDateTime &dt, const GeoLocation *geo ) {
	dms LST = geo->GSTtoLST( dt.gst() );

	//dSec is the number of seconds until the object transits.
	dms HourAngle = dms( LST.Degrees() - ra()->Degrees() );
	int dSec = int( -3600.*HourAngle.Hours() );

	//dt0 is the first guess at the transit time.
	KStarsDateTime dt0 = dt.addSecs( dSec );

	//recompute object's position at UT0 and then find
	//transit time of this refined position
	SkyPoint sp = recomputeCoords( dt0, geo );
	const dms *ram = sp.ra0();

	HourAngle = dms ( LST.Degrees() - ram->Degrees() );
	dSec = int( -3600.*HourAngle.Hours() );

	return dt.addSecs( dSec ).time();
}

TQTime SkyObject::transitTime( const KStarsDateTime &dt, const GeoLocation *geo ) {
	return geo->UTtoLT( KStarsDateTime( dt.date(), transitTimeUT( dt, geo ) ) ).time();
}

dms SkyObject::transitAltitude( const KStarsDateTime &dt, const GeoLocation *geo ) {
	KStarsDateTime dt0 = dt;
	TQTime UT = transitTimeUT( dt, geo );
	dt0.setTime( UT );
	SkyPoint sp = recomputeCoords( dt0, geo );
	const dms *decm = sp.dec0();

	dms delta;
	delta.setRadians( asin ( sin (geo->lat()->radians()) *
				sin ( decm->radians() ) +
				cos (geo->lat()->radians()) *
				cos (decm->radians() ) ) );

	return delta;
}

double SkyObject::approxHourAngle( const dms *h0, const dms *gLat, const dms *dec ) {

	double sh0 = sin ( h0->radians() );
	double r = (sh0 - sin( gLat->radians() ) * sin(dec->radians() ))
		 / (cos( gLat->radians() ) * cos( dec->radians() ) );

	double H = acos( r )/dms::DegToRad;

	return H;
}

dms SkyObject::elevationCorrection(void) {

	/* The atmospheric refraction at the horizon shifts altitude by 
	 * - 34 arcmin = 0.5667 degrees. This value changes if the observer
	 * is above the horizon, or if the weather conditions change much.
	 *
	 * For the sun we have to add half the angular sie of the body, since
	 * the sunset is the time the upper limb of the sun disappears below
	 * the horizon, and dawn, when the upper part of the limb appears 
	 * over the horizon. The angular size of the sun = angular size of the
	 * moon = 31' 59''. 
	 *
	 * So for the sun the correction is = -34 - 16 = 50 arcmin = -0.8333
	 *
	 * This same correction should be applied to the moon however parallax
	 * is important here. Meeus states that the correction should be 
	 * 0.7275 P - 34 arcmin, where P is the moon's horizontal parallax. 
	 * He proposes a mean value of 0.125 degrees if no great accuracy 
	 * is needed.
	 */

	if ( name() == "Sun" || name() == "Moon" )
		return dms(-0.8333);
//	else if ( name() == "Moon" )
//		return dms(0.125);       
	else                             // All sources point-like.
		return dms(-0.5667);
}

SkyPoint SkyObject::recomputeCoords( const KStarsDateTime &dt, const GeoLocation *geo ) {
	//store current position
	SkyPoint original( ra(), dec() );

	// compute coords for new time jd
	KSNumbers num( dt.djd() );
	if ( isSolarSystem() && geo ) {
		dms LST = geo->GSTtoLST( dt.gst() );
		updateCoords( &num, true, geo->lat(), &LST );
	} else {
		updateCoords( &num );
	}

	//the coordinates for the date dt:
	SkyPoint sp = SkyPoint( ra(), dec() );

	// restore original coords
	setRA( original.ra()->Hours() );
	setDec( original.dec()->Degrees() );

	return sp;
}

bool SkyObject::checkCircumpolar( const dms *gLat ) {
	double r = -1.0 * tan( gLat->radians() ) * tan( dec()->radians() );
	if ( r < -1.0 || r > 1.0 )
		return true;
	else
		return false;
}

TQString SkyObject::typeName( void ) const {
	if ( Type==0 ) return i18n( "Star" );
	else if ( Type==1 ) return i18n( "Catalog Star" );
	else if ( Type==2 ) return i18n( "Planet" );
	else if ( Type==3 ) return i18n( "Open Cluster" );
	else if ( Type==4 ) return i18n( "Globular Cluster" );
	else if ( Type==5 ) return i18n( "Gaseous Nebula" );
	else if ( Type==6 ) return i18n( "Planetary Nebula" );
	else if ( Type==7 ) return i18n( "Supernova Remnant" );
	else if ( Type==8 ) return i18n( "Galaxy" );
	else if ( Type==9 ) return i18n( "Comet" );
	else if ( Type==10 ) return i18n( "Asteroid" );
	else return i18n( "Unknown Type" );
}
void SkyObject::setName( const TQString &name ) {
//	if (name == "star" ) kdDebug() << "name == star" << endl;
	delete Name;
	if (!name.isEmpty())
		Name = new TQString(name);
	else
		{ Name = 0; /*kdDebug() << "name saved" << endl;*/ }
}

void SkyObject::setName2( const TQString &name2 ) {
	delete Name2;
	if (!name2.isEmpty())
		Name2 = new TQString(name2);
	else
		{ Name2 = 0; /*kdDebug() << "name2 saved" << endl;*/ }
}

TQString SkyObject::messageFromTitle( const TQString &imageTitle ) {
	TQString message = imageTitle;

	//HST Image
	if ( imageTitle == i18n( "Show HST Image" ) || imageTitle.contains("HST") ) {
		message = i18n( "%1: Hubble Space Telescope, operated by STScI for NASA [public domain]" ).arg( longname() );  

	//Spitzer Image
	} else if ( imageTitle.contains( i18n( "Show Spitzer Image" ) ) ) {
		message = i18n( "%1: Spitzer Space Telescope, courtesy NASA/JPL-Caltech [public domain]" ).arg( longname() );  

	//SEDS Image
	} else if ( imageTitle == i18n( "Show SEDS Image" ) ) {
		message = i18n( "%1: SEDS, http://www.seds.org [free for non-commercial use]" ).arg( longname() );

	//Kitt Peak AOP Image
	} else if ( imageTitle == i18n( "Show KPNO AOP Image" ) ) {
		message = i18n( "%1: Advanced Observing Program at Kitt Peak National Observatory [free for non-commercial use; no physical reproductions]" ).arg( longname() );

	//NOAO Image
	} else if ( imageTitle.contains( i18n( "Show NOAO Image" ) ) ) {
		message = i18n( "%1: National Optical Astronomy Observatories and AURA [free for non-commercial use]" ).arg( longname() );

	//VLT Image
	} else if ( imageTitle.contains( "VLT" ) ) {
		message = i18n( "%1: Very Large Telescope, operated by the European Southern Observatory [free for non-commercial use; no reproductions]" ).arg( longname() );

	//All others
	} else if ( imageTitle.startsWith( i18n( "Show" ) ) ) {
		message = imageTitle.mid( imageTitle.find( " " ) + 1 ); //eat first word, "Show"
		message = longname() + ": " + message;
	}
	
	return message;
}

//New saveUserLog, moved from DetailDialog.  
//Should create a special UserLog widget that encapsulates the "default"
//message in the widget when no log exists (much like we do with dmsBox now)
void SkyObject::saveUserLog( const TQString &newLog ) {
	TQFile file;
	TQString logs; //existing logs
	
	//Do nothing if new log is the "default" message
	//(keep going if new log is empty; we'll want to delete its current entry)
	if ( newLog == (i18n("Record here observation logs and/or data on %1.").arg(name())) || newLog.isEmpty() )
		return;

	// header label
	TQString KSLabel ="[KSLABEL:" + name() + "]";
	//However, we can't accept a star name if it has a greek letter in it:
	if ( type() == STAR ) {
		StarObject *star = (StarObject*)this;
		if ( name() == star->gname() ) 
			KSLabel = "[KSLABEL:" + star->gname( false ) + "]"; //"false": spell out greek letter
	}
	
	file.setName( locateLocal( "appdata", "userlog.dat" ) ); //determine filename in local user KDE directory tree.
	if ( file.open( IO_ReadOnly)) {
		TQTextStream instream(&file);
		// read all data into memory
		logs = instream.read();
		file.close();
	}
	
	//Remove old log entry from the logs text
	if ( ! userLog.isEmpty() ) {
		int startIndex, endIndex;
		TQString sub;
	
		startIndex = logs.find(KSLabel);
		sub = logs.mid (startIndex);
		endIndex = sub.find("[KSLogEnd]");
	
		logs.remove(startIndex, endIndex + 11);
	}
	
	//append the new log entry to the end of the logs text,
	//but only if the log is not empty
	if ( ! newLog.stripWhiteSpace().isEmpty() )
		logs.append( KSLabel + "\n" + newLog + "\n[KSLogEnd]\n" );
	
	//Open file for writing
	//FIXME: change error message to "cannot write to user log file"
	if ( !file.open( IO_WriteOnly ) ) {
		kdDebug() << i18n( "user log file could not be opened." ) << endl;
		return;
	}
	
	//Write new logs text
	TQTextStream outstream(&file);
	outstream << logs;
	
	//Set the log text in the object itself.
	userLog = newLog;
	
	file.close();
}
