/*
 
* Java GPX Library (@__identifier__@).
 
* Copyright (c) @__year__@ Franz Wilhelmstötter
 
*
 
* Licensed under the Apache License, Version 2.0 (the "License");
 
* you may not use this file except in compliance with the License.
 
* You may obtain a copy of the License at
 
*
 
*
      
http://www.apache.org/licenses/LICENSE-2.0
 
*
 
* Unless required by applicable law or agreed to in writing, software
 
* distributed under the License is distributed on an "AS IS" BASIS,
 
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 
* See the License for the specific language governing permissions and
 
* limitations under the License.
 
*
 
* Author:
 
*
    
Franz Wilhelmstötter (franz.wilhelmstoetter@gmail.com)
 
*/
package io.jenetics.jpx;

import static java.lang.String.format;
import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;
import static io.jenetics.jpx.Format.intString;
import static io.jenetics.jpx.Lists.copy;
import static io.jenetics.jpx.Lists.immutable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.net.URI;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.jenetics.jpx.GPX.Version;

/**
 
* Represents a GPX track - an ordered list of points describing a path.
 
* <p>
 
* Creating a Track object with one track-segment and 3 track-points:
 
* <pre>{@code
 
* final Track track = Track.builder()
 
*
     
.name("Track 1")
 
*
     
.description("Mountain bike tour.")
 
*
     
.addSegment(segment -> segment
 
*
         
.addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
 
*
         
.addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
 
*
         
.addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
 
*
     
.addSegment(segment -> segment
 
*
         
.addPoint(p -> p.lat(46.2081743).lon(16.3738189).ele(160))
 
*
         
.addPoint(p -> p.lat(47.2081743).lon(16.3738189).ele(161))
 
*
         
.addPoint(p -> p.lat(49.2081743).lon(16.3738189).ele(162))))
 
*
     
.build();
 
* }</pre>
 
*
 
* @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
 
* @version 1.3
 
* @since 1.0
 
*/

public final class Track implements Iterable<TrackSegment>, Serializable {

	
private static final long serialVersionUID = 2L;

	
private final String _name;
	
private final String _comment;
	
private final String _description;
	
private final String _source;
	
private final List<Link> _links;
	
private final UInt _number;
	
private final String _type;
	
private final List<TrackSegment> _segments;

	
/**
	 
* Create a new {@code Track} with the given parameters.
	 
*
	 
* @param name the GPS name of the track
	 
* @param comment the GPS comment for the track
	 
* @param description user description of the track
	 
* @param source the source of data. Included to give user some idea of
	 
*
        
reliability and accuracy of data.
	 
* @param links the links to external information about track
	 
* @param number the GPS track number
	 
* @param type the type (classification) of track
	 
* @param segments the track-segments holds a list of track-points which are
	 
*
        
logically connected in order. To represent a single GPS track
	 
*
        
where GPS reception was lost, or the GPS receiver was turned off,
	 
*
        
start a new track-segment for each continuous span of track data.
	 
*/

	
private Track(
		
final String name,
		
final String comment,
		
final String description,
		
final String source,
		
final List<Link> links,
		
final UInt number,
		
final String type,
		
final List<TrackSegment> segments
	
) {
		
_name = name;
		
_comment = comment;
		
_description = description;
		
_source = source;
		
_links = immutable(links);
		
_number = number;
		
_type = type;
		
_segments = immutable(segments);
	
}

	
/**
	 
* Return the track name.
	 
*
	 
* @return the track name
	 
*/

	
public Optional<String> getName() {
		
return Optional.ofNullable(_name);
	
}

	
/**
	 
* Return the GPS comment of the track.
	 
*
	 
* @return the GPS comment of the track
	 
*/

	
public Optional<String> getComment() {
		
return Optional.ofNullable(_comment);
	
}

	
/**
	 
* Return the text description of the track.
	 
*
	 
* @return the text description of the track
	 
*/

	
public Optional<String> getDescription() {
		
return Optional.ofNullable(_description);
	
}

	
/**
	 
* Return the source of data. Included to give user some idea of reliability
	 
* and accuracy of data.
	 
*
	 
* @return the source of data
	 
*/

	
public Optional<String> getSource() {
		
return Optional.ofNullable(_source);
	
}

	
/**
	 
* Return the links to external information about the track.
	 
*
	 
* @return the links to external information about the track
	 
*/

	
public List<Link> getLinks() {
		
return _links;
	
}

	
/**
	 
* Return the GPS track number.
	 
*
	 
* @return the GPS track number
	 
*/

	
public Optional<UInt> getNumber() {
		
return Optional.ofNullable(_number);
	
}

	
/**
	 
* Return the type (classification) of the track.
	 
*
	 
* @return the type (classification) of the track
	 
*/

	
public Optional<String> getType() {
		
return Optional.ofNullable(_type);
	
}

	
/**
	 
* Return the sequence of route points.
	 
*
	 
* @return the sequence of route points
	 
*/

	
public List<TrackSegment> getSegments() {
		
return _segments;
	
}

	
/**
	 
* Return a stream of {@link TrackSegment} objects this track contains.
	 
*
	 
* @return a stream of {@link TrackSegment} objects this track contains
	 
*/

	
public Stream<TrackSegment> segments() {
		
return _segments.stream();
	
}

	
@Override
	
public Iterator<TrackSegment> iterator() {
		
return _segments.iterator();
	
}

	
/**
	 
* Convert the <em>immutable</em> track object into a <em>mutable</em>
	 
* builder initialized with the current track values.
	 
*
	 
* @since 1.1
	 
*
	 
* @return a new track builder initialized with the values of {@code this}
	 
*
         
track
	 
*/

	
public Builder toBuilder() {
		
return builder()
			
.name(_name)
			
.cmt(_comment)
			
.desc(_description)
			
.src(_source)
			
.links(_links)
			
.number(_number)
			
.segments(_segments);
	
}

	
/**
	 
* Return {@code true} if all track properties are {@code null} or empty.
	 
*
	 
* @return {@code true} if all track properties are {@code null} or empty
	 
*/

	
public boolean isEmpty() {
		
return _name == null &&
			
_comment == null &&
			
_description == null &&
			
_source == null &&
			
_links.isEmpty() &&
			
_number == null &&
			
(_segments.isEmpty() ||
				
_segments.stream().allMatch(TrackSegment::isEmpty));
	
}

	
/**
	 
* Return {@code true} if not all track properties are {@code null} or empty.
	 
*
	 
* @since 1.1
	 
*
	 
* @return {@code true} if not all track properties are {@code null} or empty
	 
*/

	
public boolean nonEmpty() {
		
return !isEmpty();
	
}

	
@Override
	
public int hashCode() {
		
int hash = 31;
		
hash += 17*Objects.hashCode(_name) + 37;
		
hash += 17*Objects.hashCode(_comment) + 37;
		
hash += 17*Objects.hashCode(_description) + 37;
		
hash += 17*Objects.hashCode(_source) + 37;
		
hash += 17*Objects.hashCode(_type) + 37;
		
hash += 17*Lists.hashCode(_links) + 37;
		
hash += 17*Objects.hashCode(_number) + 37;
		
hash += 17*Objects.hashCode(_segments) + 37;

		
return hash;
	
}

	
@Override
	
public boolean equals(final Object obj) {
		
return obj == this ||
			
obj instanceof Track &&
			
Objects.equals(((Track)obj)._name, _name) &&
			
Objects.equals(((Track)obj)._comment, _comment) &&
			
Objects.equals(((Track)obj)._description, _description) &&
			
Objects.equals(((Track)obj)._source, _source) &&
			
Objects.equals(((Track)obj)._type, _type) &&
			
Lists.equals(((Track)obj)._links, _links) &&
			
Objects.equals(((Track)obj)._number, _number) &&
			
Objects.equals(((Track)obj)._segments, _segments);
	
}

	
@Override
	
public String toString() {
		
return format("Track[name=%s, segments=%s]", _name, _segments);
	
}

	
/**
	 
* Builder class for creating immutable {@code Track} objects.
	 
* <p>
	 
* Creating a Track object with one track-segment and 3 track-points:
	 
* <pre>{@code
	 
* final Track track = Track.builder()
	 
*
     
.name("Track 1")
	 
*
     
.description("Mountain bike tour.")
	 
*
     
.addSegment(segment -> segment
	 
*
         
.addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
	 
*
         
.addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
	 
*
         
.addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
	 
*
     
.addSegment(segment -> segment
	 
*
         
.addPoint(p -> p.lat(46.2081743).lon(16.3738189).ele(160))
	 
*
         
.addPoint(p -> p.lat(47.2081743).lon(16.3738189).ele(161))
	 
*
         
.addPoint(p -> p.lat(49.2081743).lon(16.3738189).ele(162))))
	 
*
     
.build();
	 
* }</pre>
	 
*/

	
public static final class Builder implements Filter<TrackSegment, Track> {
		
private String _name;
		
private String _comment;
		
private String _description;
		
private String _source;
		
private final List<Link> _links = new ArrayList<>();
		
private UInt _number;
		
private String _type;
		
private final List<TrackSegment> _segments = new ArrayList<>();

		
private Builder() {
		
}

		
/**
		 
* Set the name of the track.
		 
*
		 
* @param name the track name
		 
* @return {@code this} {@code Builder} for method chaining
		 
*/

		
public Builder name(final String name) {
			
_name = name;
			
return this;
		
}

		
/**
		 
* Return the current name value.
		 
*
		 
* @since 1.1
		 
*
		 
* @return the current name value
		 
*/

		
public Optional<String> name() {
			
return Optional.ofNullable(_name);
		
}

		
/**
		 
* Set the comment of the track.
		 
*
		 
* @param comment the track comment
		 
* @return {@code this} {@code Builder} for method chaining
		 
*/

		
public Builder cmt(final String comment) {
			
_comment = comment;
			
return this;
		
}

		
public Optional<String> cmt() {
			
return Optional.ofNullable(_comment);
		
}

		
/**
		 
* Set the description of the track.
		 
*
		 
* @param description the track description
		 
* @return {@code this} {@code Builder} for method chaining
		 
*/

		
public Builder desc(final String description) {
			
_description = description;
			
return this;
		
}

		
/**
		 
* Return the current description value.
		 
*
		 
* @since 1.1
		 
*
		 
* @return the current description value
		 
*/

		
public Optional<String> desc() {
			
return Optional.ofNullable(_description);
		
}

		
/**
		 
* Set the source of the track.
		 
*
		 
* @param source the track source
		 
* @return {@code this} {@code Builder} for method chaining
		 
*/

		
public Builder src(final String source) {
			
_source = source;
			
return this;
		
}

		
/**
		 
* Return the current source value.
		 
*
		 
* @since 1.1
		 
*
		 
* @return the current source value
		 
*/

		
public Optional<String> src() {
			
return Optional.ofNullable(_source);
		
}

		
/**
		 
* Set the track links. The link list may be {@code null}.
		 
*
		 
* @param links the track links
		 
* @return {@code this} {@code Builder} for method chaining
		 
* @throws NullPointerException if one of the links in the list is
		 
*
         
{@code null}
		 
*/

		
public Builder links(final List<Link> links) {
			
copy(links, _links);
			
return this;
		
}

		
/**
		 
* Add the given {@code link} to the track
		 
*
		 
* @param link the link to add to the track
		 
* @return {@code this} {@code Builder} for method chaining
		 
*/

		
public Builder addLink(final Link link) {
			
_links.add(requireNonNull(link));

			
return this;
		
}

		
/**
		 
* Add the given {@code link} to the track
		 
*
		 
* @param href the link to add to the track
		 
* @return {@code this} {@code Builder} for method chaining
		 
* @throws NullPointerException if the given {@code href} is {@code null}
		 
* @throws IllegalArgumentException if the given {@code href} is not a
		 
*
         
valid URL
		 
*/

		
public Builder addLink(final String href) {
			
return addLink(Link.of(href));
		
}

		
/**
		 
* Return the current links. The returned link list is mutable.
		 
*
		 
* @since 1.1
		 
*
		 
* @return the current links
		 
*/

		
public List<Link> links() {
			
return new NonNullList<>(_links);
		
}

		
/**
		 
* Set the track number.
		 
*
		 
* @param number the track number
		 
* @return {@code this} {@code Builder} for method chaining
		 
*/

		
public Builder number(final UInt number) {
			
_number = number;
			
return this;
		
}

		
/**
		 
* Set the track number.
		 
*
		 
* @param number the track number
		 
* @return {@code this} {@code Builder} for method chaining
		 
* @throws IllegalArgumentException if the given {@code value} is smaller
		 
*
         
than zero
		 
*/

		
public Builder number(final int number) {
			
_number = UInt.of(number);
			
return this;
		
}

		
/**
		 
* Return the current number value.
		 
*
		 
* @since 1.1
		 
*
		 
* @return the current number value
		 
*/

		
public Optional<UInt> number() {
			
return Optional.ofNullable(_number);
		
}

		
/**
		 
* Set the track type.
		 
*
		 
* @param type the track type
		 
* @return {@code this} {@code Builder} for method chaining
		 
*/

		
public Builder type(final String type) {
			
_type = type;
			
return this;
		
}

		
/**
		 
* Return the current type value.
		 
*
		 
* @since 1.1
		 
*
		 
* @return the current type value
		 
*/

		
public Optional<String> type() {
			
return Optional.ofNullable(_type);
		
}

		
/**
		 
* Set the track segments of the track. The list may be {@code null}.
		 
*
		 
* @param segments the track segments
		 
* @return {@code this} {@code Builder} for method chaining
		 
* @throws NullPointerException if one of the segments in the list is
		 
*
         
{@code null}
		 
*/

		
public Builder segments(final List<TrackSegment> segments) {
			
copy(segments, _segments);
			
return this;
		
}

		
/**
		 
* Add a track segment to the track.
		 
*
		 
* @param segment the track segment added to the track
		 
* @return {@code this} {@code Builder} for method chaining
		 
* @throws NullPointerException if the given argument is {@code null}
		 
*/

		
public Builder addSegment(final TrackSegment segment) {
			
_segments.add(requireNonNull(segment));

			
return this;
		
}

		
/**
		 
* Add a track segment to the track, via the given builder.
		 
* <pre>{@code
		 
* final Track track = Track.builder()
		 
*
     
.name("Track 1")
		 
*
     
.description("Mountain bike tour.")
		 
*
     
.addSegment(segment -> segment
		 
*
         
.addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
		 
*
         
.addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
		 
*
         
.addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
		 
*
     
.addSegment(segment -> segment
		 
*
         
.addPoint(p -> p.lat(46.2081743).lon(16.3738189).ele(160))
		 
*
         
.addPoint(p -> p.lat(47.2081743).lon(16.3738189).ele(161))
		 
*
         
.addPoint(p -> p.lat(49.2081743).lon(16.3738189).ele(162))))
		 
*
     
.build();
		 
* }</pre>
		 
*
		 
* @param segment the track segment
		 
* @return {@code this} {@code Builder} for method chaining
		 
* @throws NullPointerException if the given argument is {@code null}
		 
*/

		
public Builder addSegment(final Consumer<TrackSegment.Builder> segment) {
			
final TrackSegment.Builder builder = TrackSegment.builder();
			
segment.accept(builder);
			
return addSegment(builder.build());
		
}

		
/**
		 
* Return the current track segments. The returned segment list is
		 
* mutable.
		 
*
		 
* @since 1.1
		 
*
		 
* @return the current track segments
		 
*/

		
public List<TrackSegment> segments() {
			
return new NonNullList<>(_segments);
		
}

		
@Override
		
public Builder filter(final Predicate<? super TrackSegment> predicate) {
			
segments(
				
_segments.stream()
					
.filter(predicate)
					
.collect(Collectors.toList())
			
);

			
return this;
		
}

		
@Override
		
public Builder map(
			
final Function<? super TrackSegment, ? extends TrackSegment> mapper
		
) {
			
segments(
				
_segments.stream()
					
.map(mapper)
					
.collect(Collectors.toList())
			
);

			
return this;
		
}

		
@Override
		
public Builder flatMap(
			
final Function<
				
? super TrackSegment,
				
? extends List<TrackSegment>> mapper
		
) {
			
segments(
				
_segments.stream()
					
.flatMap(segment -> mapper.apply(segment).stream())
					
.collect(Collectors.toList())
			
);

			
return this;
		
}

		
@Override
		
public Builder listMap(
			
final Function<
				
? super List<TrackSegment>,
				
? extends List<TrackSegment>> mapper
		
) {
			
segments(mapper.apply(_segments));

			
return this;
		
}

		
/**
		 
* Create a new GPX track from the current builder state.
		 
*
		 
* @return a new GPX track from the current builder state
		 
*/

		
@Override
		
public Track build() {
			
return new Track(
				
_name,
				
_comment,
				
_description,
				
_source,
				
_links,
				
_number,
				
_type,
				
_segments
			
);
		
}
	
}

	
public static Builder builder() {
		
return new Builder();
	
}


	
/* *************************************************************************
	 
*
  
Static object creation methods
	 
* ************************************************************************/


	
/**
	 
* Create a new {@code Track} with the given parameters.
	 
*
	 
* @param name the GPS name of the track
	 
* @param comment the GPS comment for the track
	 
* @param description user description of the track
	 
* @param source the source of data. Included to give user some idea of
	 
*
        
reliability and accuracy of data.
	 
* @param links the links to external information about track
	 
* @param number the GPS track number
	 
* @param type the type (classification) of track
	 
* @param segments the track-segments holds a list of track-points which are
	 
*
        
logically connected in order. To represent a single GPS track
	 
*
        
where GPS reception was lost, or the GPS receiver was turned off,
	 
*
        
start a new track-segment for each continuous span of track data.
	 
* @return a new {@code Track} with the given parameters
	 
* @throws NullPointerException if the {@code links} or the {@code segments}
	 
*
         
sequence is {@code null}
	 
*/

	
public static Track of(
		
final String name,
		
final String comment,
		
final String description,
		
final String source,
		
final List<Link> links,
		
final UInt number,
		
final String type,
		
final List<TrackSegment> segments
	
) {
		
return new Track(
			
name,
			
comment,
			
description,
			
source,
			
links,
			
number,
			
type,
			
segments
		
);
	
}


	
/* *************************************************************************
	 
*
  
Java object serialization
	 
* ************************************************************************/


	
private Object writeReplace() {
		
return new Serial(Serial.TRACK, this);
	
}

	
private void readObject(final ObjectInputStream stream)
		
throws InvalidObjectException
	
{
		
throw new InvalidObjectException("Serialization proxy required.");
	
}

	
void write(final DataOutput out) throws IOException {
		
IO.writeNullableString(_name, out);
		
IO.writeNullableString(_comment, out);
		
IO.writeNullableString(_description, out);
		
IO.writeNullableString(_source, out);
		
IO.writes(_links, Link::write, out);
		
IO.writeNullable(_number, UInt::write, out);
		
IO.writeNullableString(_type, out);
		
IO.writes(_segments, TrackSegment::write, out);
	
}

	
static Track read(final DataInput in) throws IOException {
		
return new Track(
			
IO.readNullableString(in),
			
IO.readNullableString(in),
			
IO.readNullableString(in),
			
IO.readNullableString(in),
			
IO.reads(Link::read, in),
			
IO.readNullable(UInt::read, in),
			
IO.readNullableString(in),
			
IO.reads(TrackSegment::read, in)
		
);
	
}

	
/* *************************************************************************
	 
*
  
XML stream object serialization
	 
* ************************************************************************/


	
private static String url(final Track track) {
		
return track.getLinks().isEmpty()
			
? null
			
: track.getLinks().get(0).getHref().toString();
	
}

	
private static String urlname(final Track track) {
		
return track.getLinks().isEmpty()
			
? null
			
: track.getLinks().get(0).getText().orElse(null);
	
}

	
// Define the needed writers for the different versions.
	
private static final XMLWriters<Track> WRITERS = new XMLWriters<Track>()
		
.v00(XMLWriter.elem("name").map(t -> t._name))
		
.v00(XMLWriter.elem("cmt").map(r -> r._comment))
		
.v00(XMLWriter.elem("desc").map(r -> r._description))
		
.v00(XMLWriter.elem("src").map(r -> r._source))
		
.v11(XMLWriter.elems(Link.WRITER).map(r -> r._links))
		
.v10(XMLWriter.elem("url").map(Track::url))
		
.v10(XMLWriter.elem("urlname").map(Track::urlname))
		
.v00(XMLWriter.elem("number").map(r -> intString(r._number)))
		
.v00(XMLWriter.elem("type").map(r -> r._type))
		
.v10(XMLWriter.elems(TrackSegment.xmlWriter(Version.V10)).map(r -> r._segments))
		
.v11(XMLWriter.elems(TrackSegment.xmlWriter(Version.V11)).map(r -> r._segments));

	
// Define the needed readers for the different versions.
	
private static final XMLReaders READERS = new XMLReaders()
		
.v00(XMLReader.elem("name"))
		
.v00(XMLReader.elem("cmt"))
		
.v00(XMLReader.elem("desc"))
		
.v00(XMLReader.elem("src"))
		
.v11(XMLReader.elems(Link.READER))
		
.v10(XMLReader.elem("url").map(Format::parseURI))
		
.v10(XMLReader.elem("urlname"))
		
.v00(XMLReader.elem("number").map(UInt::parse))
		
.v00(XMLReader.elem("type"))
		
.v10(XMLReader.elems(TrackSegment.xmlReader(Version.V10)))
		
.v11(XMLReader.elems(TrackSegment.xmlReader(Version.V11)))
		
.v00(XMLReader.ignore("extensions"));

	
static XMLWriter<Track> xmlWriter(final Version version) {
		
return XMLWriter.elem("trk", WRITERS.writers(version));
	
}

	
@SuppressWarnings("unchecked")
	
static XMLReader<Track> xmlReader(final Version version) {
		
return XMLReader.elem(
			
version == Version.V10 ? Track::toTrackV10 : Track::toTrackV11,
			
"trk",
			
READERS.readers(version)
		
);
	
}

	
@SuppressWarnings("unchecked")
	
private static Track toTrackV11(final Object[] v) {
		
return Track.of(
			
(String)v[0],
			
(String)v[1],
			
(String)v[2],
			
(String)v[3],
			
(List<Link>)v[4],
			
(UInt)v[5],
			
(String)v[6],
			
(List<TrackSegment>)v[7]
		
);
	
}

	
@SuppressWarnings("unchecked")
	
private static Track toTrackV10(final Object[] v) {
		
return Track.of(
			
(String)v[0],
			
(String)v[1],
			
(String)v[2],
			
(String)v[3],
			
v[4] != null
				
? singletonList(Link.of((URI)v[4], (String)v[5], null))
				
: null,
			
(UInt)v[6],
			
(String)v[7],
			
(List<TrackSegment>)v[8]
		
);
	
}

}